using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Threading; using UnitTestSharp; namespace Vellum.UnitTests { public class SaveConvertersTests : TestFixture { public static SaveConverters TestConverters() { return SaveConverters.For(typeof(SaveConvertersTests).Assembly); } /// /// Writes as Lua text, inside the table a save has to be, and reads it back. /// public static SaveNode ThroughLuaText(SaveNode node) { SaveNode save = SaveNode.NewTable(); save.Entries["value"] = node; return LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).RequireEntry("value"); } public static string[] IssuePaths(SaveSession session) { return session.Sandbox.Issues.Select(issue => issue.Path).ToArray(); } /// /// Returns a chain of links, the last with no next. /// public static Link Chain(int count) { Link returnMe = null; for (int i = 0; i < count; ++i) { returnMe = new Link { Next = returnMe }; } return returnMe; } /// /// Returns the node levels down from , following the entry /// named at each level. /// public static SaveNode Descend(SaveNode node, string key, int steps) { SaveNode returnMe = node.Resolved; for (int i = 0; i < steps; ++i) { returnMe = returnMe.RequireEntry(key).Resolved; } return returnMe; } /// /// Returns how many tables lie inside one another from down, following the entry named /// . /// public static int DepthOf(SaveNode node, string key) { int returnMe = 0; for (SaveNode next = node.Resolved; next.Kind == SaveNode.Kinds.Table; next = next.Entries[key].Resolved) { ++returnMe; } return returnMe; } /// /// Returns converters for Point and for the generic types the test converters handle. /// public static SaveConverters WithGenericConverters() { return new SaveConverters(new SaveConverter[] { new PointConverter() }, new[] { typeof(BoxConverter<>), typeof(PairConverter<,>), typeof(CellConverter<>), typeof(CountedConverter<>), }); } public static SaveSession Session() { return TestConverters().CreateSession(); } public static SaveSession GenericSession() { return WithGenericConverters().CreateSession(); } /// /// Returns a session for converters of nothing but the open converters given. /// public static SaveSession SessionWith(params Type[] openConverters) { return new SaveConverters(new SaveConverter[0], openConverters).CreateSession(); } /// /// Returns a chain of cells, the last with no next. /// public static Cell CellChain(int count) { Cell returnMe = null; for (int i = 0; i < count; ++i) { returnMe = new Cell { Next = returnMe }; } return returnMe; } /// /// Returns a converter for a new public type named , in an assembly of its own. /// public static SaveConverter ConverterForANewType(string typeName) { AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName("Holds" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.Run); Type type = assembly.DefineDynamicModule("Module").DefineType(typeName, TypeAttributes.Public).CreateType(); return (SaveConverter)Activator.CreateInstance(typeof(StubConverter<>).MakeGenericType(type)); } public class CtorTests : TestFixture { public void TwoConvertersForDifferentTypesOfOneName_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new[] { ConverterForANewType("Zoo.Twin"), ConverterForANewType("Zoo.Twin") }); } public void TwoGenericConvertersForOneGenericType_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(BoxConverter<>), typeof(StructBoxConverter<>) }); } public void GenericConverterOfArrays_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(ArrayOfParameterConverter<>) }); } public void GenericConverterOfLists_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(ListConverter<>) }); } public void GenericConverterOfMultiDimensionalArrays_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(MultiDimensionalConverter<>) }); } public void GenericConverterOfQueues_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(QueueConverter<>) }); } public void GenericConverterOfStacks_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(StackConverter<>) }); } public void GenericConverterOfSets_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(HashSetConverter<>) }); } public void GenericConverterOfDictionaries_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(DictionaryConverter<,>) }); } public void GenericConverterOfNullable_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(NullableOfAnyConverter<>) }); } public void GenericConverterWhoseTypeCantBeMatched_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(AnyTypeConverter<>) }); } public void TwoFamiliesOfOneBaseClass_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(ThrowingCreatureConverter<>) }); } public void TwoFamiliesOfOneGenericBaseClass_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[0], new[] { typeof(HerdConverter<,>), typeof(StructHerdConverter<,>) }); } public void FamiliesOfDifferentInterfaces_AreAccepted() { new SaveConverters(new SaveConverter[0], new[] { typeof(CarnivoreConverter<>), typeof(PredatorConverter<>), typeof(AquaticConverter<>) }); } public void FamiliesOfDifferentBaseClasses_AreAccepted() { new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(WolfConverter<>), typeof(HerdConverter<,>) }); } public void TwoConvertersForOneType_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter(), new StubConverter() }); } public void ConverterForAnInterface_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForABuiltInType_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForAnEnum_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForANullableEnum_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForANullableBuiltInType_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForAnArray_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForAList_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter>() }); } public void ConverterForASet_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter>() }); } public void ConverterForADictionary_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter>() }); } public void ConverterForAMultiDimensionalArray_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter() }); } public void ConverterForAQueue_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter>() }); } public void ConverterForAStack_Throws() { CheckThrow(typeof(InvalidOperationException)); new SaveConverters(new SaveConverter[] { new StubConverter>() }); } public void ConverterForAnotherGenericType_IsAllowed() { new SaveConverters(new SaveConverter[] { new StubConverter>() }); } } public class BuiltInCollectionsTests : TestFixture { public void HoldsTheNineGenericCollectionsVellumSaves() { var expected = new HashSet { typeof(List<>), typeof(LinkedList<>), typeof(HashSet<>), typeof(SortedSet<>), typeof(Queue<>), typeof(Stack<>), typeof(Dictionary<,>), typeof(SortedDictionary<,>), typeof(SortedList<,>), }; CheckEqual(9, SaveConverters.BuiltInCollections.Count); CheckTrue(expected.SetEquals(SaveConverters.BuiltInCollections)); } public void EachIsABuiltInCollection() { foreach (Type definition in SaveConverters.BuiltInCollections) { CheckTrue(SaveConverters.IsBuiltInCollection(definition)); } } } public class IsBuiltInCollectionTests : TestFixture { public void OneDimensionalArray_Is() { CheckTrue(SaveConverters.IsBuiltInCollection(typeof(int[]))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(string[][]))); } public void ArrayOfTwoOrThreeDimensions_Is() { CheckTrue(SaveConverters.IsBuiltInCollection(typeof(int[,]))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(string[,,]))); } public void ArrayOfAnyNumberOfDimensions_Is() { CheckTrue(SaveConverters.IsBuiltInCollection(typeof(int[,,,]))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(int).MakeArrayType(32))); } public void ArrayThatDoesntStartAtTheOrdinaryZero_IsNot() { CheckFalse(SaveConverters.IsBuiltInCollection(typeof(int).MakeArrayType(1))); } public void ListSetQueueStackAndDictionary_Are() { CheckTrue(SaveConverters.IsBuiltInCollection(typeof(List))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(HashSet))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Queue))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Stack))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(LinkedList))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedSet))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedDictionary))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedList))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Dictionary>))); } public void TheirOpenDefinitions_Are() { CheckTrue(SaveConverters.IsBuiltInCollection(typeof(List<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(HashSet<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Queue<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Stack<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(LinkedList<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedSet<>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(Dictionary<,>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedDictionary<,>))); CheckTrue(SaveConverters.IsBuiltInCollection(typeof(SortedList<,>))); } public void OtherGenericTypes_AreNot() { CheckFalse(SaveConverters.IsBuiltInCollection(typeof(Collection))); CheckFalse(SaveConverters.IsBuiltInCollection(typeof(Box))); CheckFalse(SaveConverters.IsBuiltInCollection(typeof(int?))); } public void TypesThatArentGeneric_AreNot() { CheckFalse(SaveConverters.IsBuiltInCollection(typeof(int))); CheckFalse(SaveConverters.IsBuiltInCollection(typeof(Point))); } } public class FromTests : TestFixture { public void ConvertersAndGenericConverters_AreBothUsed() { SaveSession session = SaveConverters.From( new[] { typeof(PointConverter), typeof(BoxConverter<>) }).CreateSession(); var value = new Box { Content = new Point(1.5, 0) }; SaveNode node = session.ToNode(value, typeof(Box)); CheckEqual("0x3FF8000000000000", node.RequireEntry("content").RequireEntry("x").Text); } public void NoConverters_SavesOnlyTheBuiltInTypes() { SaveSession session = SaveConverters.From(new Type[0]).CreateSession(); CheckEqual("5", session.ToNode(5, typeof(int)).Text); } } public static int dynamicAssemblyCount; /// /// Returns a dynamic assembly holding one converter type per entry of , each given /// the attributes beside its name, and no [SavedWith] list. /// public static AssemblyBuilder AssemblyWithConverters(params KeyValuePair[] kinds) { AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName("NoGenerator" + ++dynamicAssemblyCount), AssemblyBuilderAccess.Run); ModuleBuilder module = assembly.DefineDynamicModule("NoGenerator"); foreach (KeyValuePair kind in kinds) { TypeBuilder type = module.DefineType(kind.Key, kind.Value, typeof(SaveConverter)); if ((kind.Value & TypeAttributes.Abstract) == 0) { StubOverride(type, "get_ValueType", typeof(Type), Type.EmptyTypes); StubOverride(type, "ToNode", typeof(SaveNode), new[] { typeof(object), typeof(SaveSession) }); StubOverride(type, "FromNode", typeof(object), new[] { typeof(SaveNode), typeof(SaveSession) }); } type.CreateType(); } return assembly; } public static void StubOverride(TypeBuilder type, string name, Type returnType, Type[] parameters) { MethodBuilder method = type.DefineMethod(name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName, returnType, parameters); ILGenerator il = method.GetILGenerator(); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ret); } public static KeyValuePair Kind(string name, TypeAttributes attributes) { return new KeyValuePair(name, attributes); } public class TryFindConverterTests : TestFixture { public void OneDimensionalArray_GetsAnArrayConverterOfItsElementType() { SaveConverters converters = TestConverters(); SaveConverter ints; SaveConverter strings; CheckTrue(converters.TryFindConverter(typeof(int[]), out ints)); CheckTrue(converters.TryFindConverter(typeof(string[]), out strings)); CheckTrue(ints.GetType() == typeof(ArrayConverter)); CheckTrue(strings.GetType() == typeof(ArrayConverter)); } public void ArrayOfArrays_GetsAnArrayConverterOfTheInnerArray() { SaveConverter converter; CheckTrue(TestConverters().TryFindConverter(typeof(int[][]), out converter)); CheckTrue(converter.GetType() == typeof(ArrayConverter)); } public void TwoOrMoreDimensions_GetAnArrayConverterOfTheirElementType() { SaveConverters converters = TestConverters(); SaveConverter table; SaveConverter cube; CheckTrue(converters.TryFindConverter(typeof(int[,]), out table)); CheckTrue(converters.TryFindConverter(typeof(int[,,]), out cube)); CheckTrue(table.GetType() == typeof(ArrayConverter)); CheckTrue(cube.GetType() == typeof(ArrayConverter)); CheckTrue(table.ValueType == typeof(int[,])); CheckTrue(cube.ValueType == typeof(int[,,])); } public void SameArrayType_GetsTheSameConverter() { SaveConverters converters = TestConverters(); SaveConverter first; SaveConverter second; converters.TryFindConverter(typeof(int[]), out first); converters.TryFindConverter(typeof(int[]), out second); CheckTrue(ReferenceEquals(first, second)); } public void ArrayOfPointers_HasNoConverter() { SaveConverters converters = TestConverters(); Type pointers = typeof(int).MakePointerType().MakeArrayType(); SaveConverter converter; CheckFalse(converters.TryFindConverter(pointers, out converter)); CheckFalse(converters.TryFindConverter(pointers, out converter)); CheckNull(converter); } } public class FindPublicConverterTests : TestFixture { public void AssemblyWithAPublicConcreteConverter_ReturnsIt() { AssemblyBuilder assembly = AssemblyWithConverters(Kind("Lonely", TypeAttributes.Public)); CheckEqual("Lonely", SaveConverters.FindPublicConverter(assembly).FullName); } public void AssemblyWithNoTypes_ReturnsNull() { CheckNull(SaveConverters.FindPublicConverter(AssemblyWithConverters())); } public void AbstractConverter_IsSkipped() { AssemblyBuilder assembly = AssemblyWithConverters( Kind("Abstract", TypeAttributes.Public | TypeAttributes.Abstract)); CheckNull(SaveConverters.FindPublicConverter(assembly)); } public void NonPublicConverter_IsSkipped() { AssemblyBuilder assembly = AssemblyWithConverters(Kind("Hidden", TypeAttributes.NotPublic)); CheckNull(SaveConverters.FindPublicConverter(assembly)); } public void VellumItself_IsSkipped() { CheckNull(SaveConverters.FindPublicConverter(typeof(SaveConverters).Assembly)); } } public class ForTests : TestFixture { public void AnAssemblyWithAPublicConverterAndNoSavedWith_Throws() { AssemblyBuilder assembly = AssemblyWithConverters(Kind("Lonely", TypeAttributes.Public)); CheckThrow(typeof(InvalidOperationException)); SaveConverters.For(assembly); } public void TheForgottenGeneratorError_NamesTheAssemblyAndTheConverter() { AssemblyBuilder assembly = AssemblyWithConverters(Kind("Lonely", TypeAttributes.Public)); string message = null; try { SaveConverters.For(assembly); } catch (InvalidOperationException exception) { message = exception.Message; } CheckNotNull(message); CheckTrue(message.Contains(assembly.GetName().Name)); CheckTrue(message.Contains("Lonely")); CheckTrue(message.Contains("Vellum.Generator")); } public void AnAssemblyWithAPublicConverterThatItNames_DoesNotThrow() { AssemblyBuilder assembly = AssemblyWithConverters(Kind("Lonely", TypeAttributes.Public)); ConstructorInfo attribute = typeof(SavedWithAttribute).GetConstructor(new[] { typeof(Type) }); assembly.SetCustomAttribute(new CustomAttributeBuilder( attribute, new object[] { typeof(PointConverter) })); CheckNotNull(SaveConverters.For(assembly)); } public void AnAssemblyWithNoConvertersAtAll_DoesNotThrow() { CheckNotNull(SaveConverters.For(AssemblyWithConverters())); } public void AnAssemblyNamingAGenericConverter_SavesWithItsClosedConverters() { AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName("NamesAGenericConverter"), AssemblyBuilderAccess.Run); ConstructorInfo attribute = typeof(SavedWithAttribute).GetConstructor(new[] { typeof(Type) }); assembly.SetCustomAttribute( new CustomAttributeBuilder(attribute, new object[] { typeof(BoxConverter<>) })); SaveSession session = SaveConverters.For(assembly).CreateSession(); SaveNode node = session.ToNode(new Box { Content = 7 }, typeof(Box)); CheckEqual("7", node.RequireEntry("content").Text); } public void FindsTheConvertersTheAssemblyNames() { SaveSession session = Session(); CheckEqual(SaveNode.Kinds.Table, session.ToNode(new Point(1, 2), typeof(Point)).Kind); CheckEqual(SaveNode.Kinds.Table, session.ToNode(new Waypoint(), typeof(Waypoint)).Kind); CheckEqual(SaveNode.Kinds.Table, session.ToNode(new Link(), typeof(Link)).Kind); } public void AnAssemblyNamingNoConverters_CantSaveAConvertedType() { SaveSession session = SaveConverters.For(typeof(SaveNode).Assembly).CreateSession(); CheckThrow(typeof(NotSupportedException)); session.ToNode(new Point(1, 2), typeof(Point)); } } public class VerifyInterfaceFamiliesTests : TestFixture { public static SaveConverters WithFamilies(params Type[] families) { return new SaveConverters(new SaveConverter[0], families); } public static string ThrownMessage(SaveConverters converters, params Type[] types) { try { converters.VerifyInterfaceFamilies(types); } catch (InvalidOperationException exception) { return exception.Message; } return null; } public static Assembly AssemblyImplementing(params Type[] interfaces) { AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName("Implements" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.Run); assembly.DefineDynamicModule("Module") .DefineType("Chimera", TypeAttributes.Public, typeof(object), interfaces) .CreateType(); return assembly; } public void TypeInTwoUnrelatedFamilies_ThrowsNamingTheTypeAndBothConverters() { string message = ThrownMessage(WithFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)), typeof(Shark)); CheckNotNull(message); CheckTrue(message.Contains("Shark")); CheckTrue(message.Contains("CarnivoreConverter")); CheckTrue(message.Contains("AquaticConverter")); } public void EveryUnsaveableType_IsNamedInTheOneException() { string message = ThrownMessage(WithFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>), typeof(VesselConverter<,>)), typeof(Shark), typeof(Lynx), typeof(Twin)); CheckNotNull(message); CheckTrue(message.Contains("Shark")); CheckTrue(message.Contains("Twin")); CheckFalse(message.Contains("Lynx")); } public void TypeImplementingAGenericInterfaceTwice_ThrowsWithJustThatFamily() { string message = ThrownMessage(WithFamilies(typeof(VesselConverter<,>)), typeof(Twin)); CheckNotNull(message); CheckTrue(message.Contains("Twin")); } public void TypesEachFamilyCanChoose_DoNotThrow() { CheckNull(ThrownMessage(WithFamilies(typeof(CarnivoreConverter<>), typeof(PredatorConverter<>), typeof(AquaticConverter<>), typeof(VesselConverter<,>)), typeof(Lynx), typeof(Orca), typeof(Otter), typeof(Sprat), typeof(Cod), typeof(Barrel), typeof(Gryphon))); } public void TypesNoFamilyCouldSave_DoNotThrow() { CheckNull(ThrownMessage(WithFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)), typeof(int), typeof(string), typeof(DayOfWeek), typeof(ICarnivore), typeof(AbstractAquatic), typeof(List), typeof(int[]))); } public void NoInterfaceFamilies_DoNotThrow() { CheckNull(ThrownMessage(WithFamilies(typeof(CreatureConverter<>)), typeof(Shark), typeof(Twin))); } public void Assembly_ChecksTheTypesItDefines() { SaveConverters converters = WithFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)); CheckThrow(typeof(InvalidOperationException)); converters.VerifyInterfaceFamilies(AssemblyImplementing(typeof(ICarnivore), typeof(IAquatic))); } public void Assembly_WithNothingAmbiguous_DoesNotThrow() { WithFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)) .VerifyInterfaceFamilies(AssemblyImplementing(typeof(ICarnivore))); } } public class SubtypeContentsTests : TestFixture { public void TableWithEntries_HoldsThemAllButTheName() { SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("Vellum.UnitTests.Dog"); node.Entries["name"] = SaveNode.OfText("Rex"); node.Entries["age"] = SaveNode.OfText("3"); SaveNode contents = SaveConverters.SubtypeContents(node, "Vellum.UnitTests.Dog"); CheckEqual(2, contents.Entries.Count); CheckTrue(ReferenceEquals(node.Entries["name"], contents.Entries["name"])); CheckTrue(ReferenceEquals(node.Entries["age"], contents.Entries["age"])); } public void TableWithEntries_IsLeftAlone() { SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("Vellum.UnitTests.Dog"); SaveConverters.SubtypeContents(node, "Vellum.UnitTests.Dog"); CheckEqual(1, node.Entries.Count); } public void NameOnly_HoldsAnEmptyTable() { SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("Vellum.UnitTests.Dog"); SaveNode contents = SaveConverters.SubtypeContents(node, "Vellum.UnitTests.Dog"); CheckEqual(SaveNode.Kinds.Table, contents.Kind); CheckEqual(0, contents.Entries.Count); } public void ValueEntry_IsWhatTheNodeHolds() { SaveNode value = SaveNode.OfText("5"); SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("int"); node.Entries[SaveConverters.ValueKey] = value; CheckTrue(ReferenceEquals(value, SaveConverters.SubtypeContents(node, "int"))); } public void ValueEntryBesideOthers_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("int"); node.Entries[SaveConverters.ValueKey] = SaveNode.OfText("5"); node.Entries["extra"] = SaveNode.OfText("6"); CheckThrow(typeof(InvalidDataException)); SaveConverters.SubtypeContents(node, "int"); } } public class CreateSessionTests : TestFixture { public void ObjectSavedInTwoCalls_IsTheSameNode() { SaveSession session = Session(); var waypoint = new Waypoint { Name = "home" }; SaveNode first = session.ToNode(waypoint, typeof(Waypoint)); CheckTrue(ReferenceEquals(first, session.ToNode(waypoint, typeof(Waypoint)))); } public void ObjectSavedInAnotherSession_IsANewNode() { SaveConverters registry = TestConverters(); var waypoint = new Waypoint { Name = "home" }; SaveNode first = registry.CreateSession().ToNode(waypoint, typeof(Waypoint)); CheckFalse(ReferenceEquals(first, registry.CreateSession().ToNode(waypoint, typeof(Waypoint)))); } public void ObjectChangedBetweenTwoSaves_IsStillTheFirstNode() { SaveSession session = Session(); var waypoint = new Waypoint { Name = "home" }; SaveNode first = session.ToNode(waypoint, typeof(Waypoint)); waypoint.Name = "away"; SaveNode second = session.ToNode(waypoint, typeof(Waypoint)); CheckTrue(ReferenceEquals(first, second)); CheckEqual("home", second.RequireEntry("name").Text); } public void NodeLoadedInTwoCalls_IsTheSameObject() { SaveConverters registry = TestConverters(); SaveNode node = registry.CreateSession().ToNode(new Waypoint { Name = "home" }, typeof(Waypoint)); SaveSession session = registry.CreateSession(); object first = session.FromNode(node, typeof(Waypoint)); CheckTrue(ReferenceEquals(first, session.FromNode(node, typeof(Waypoint)))); } public void NodeLoadedInAnotherSession_IsAnotherObject() { SaveConverters registry = TestConverters(); SaveNode node = registry.CreateSession().ToNode(new Waypoint { Name = "home" }, typeof(Waypoint)); object first = registry.CreateSession().FromNode(node, typeof(Waypoint)); CheckFalse(ReferenceEquals(first, registry.CreateSession().FromNode(node, typeof(Waypoint)))); } public void NodeLoadedAsTwoTypes_Throws() { SaveConverters registry = TestConverters(); SaveNode node = registry.CreateSession().ToNode(new Waypoint { Name = "home" }, typeof(Waypoint)); SaveSession session = registry.CreateSession(); session.FromNode(node, typeof(Waypoint)); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(Link)); } public void SessionsOnSeveralThreads_EachSaveAndLoadTheirOwnValues() { const int threadCount = 4; const int roundsPerThread = 20; SaveConverters registry = WithGenericConverters(); var start = new Barrier(threadCount); var failures = new List(); var threads = new List(); for (int thread = 0; thread < threadCount; ++thread) { int seed = thread; threads.Add(new Thread(() => { try { start.SignalAndWait(); for (int round = 0; round < roundsPerThread; ++round) { SaveSession session = registry.CreateSession(); var boxed = new Box> { Content = new List { new Point(seed, round) }, }; var pair = new Pair { First = seed, Second = "round" + round }; var loadedBox = (Box>)session.FromNode( session.ToNode(boxed, typeof(Box>)), typeof(Box>)); var loadedPair = (Pair)session.FromNode( session.ToNode(pair, typeof(Pair)), typeof(Pair)); if (loadedBox.Content[0].X != seed || loadedBox.Content[0].Y != round || loadedPair.First != seed || loadedPair.Second != "round" + round) { throw new InvalidOperationException("A thread got another thread's values."); } } } catch (Exception exception) { lock (failures) { failures.Add(exception); } } })); } foreach (Thread thread in threads) { thread.Start(); } foreach (Thread thread in threads) { thread.Join(); } CheckEqual(0, failures.Count); } } public class PublishTests : TestFixture { public static SaveNode EmptyChair() { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["twin"] = SaveNode.Null; return returnMe; } public void ObjectPublishedForANode_IsWhatLoadingThatNodeReturns() { SaveSession session = Session(); SaveNode node = EmptyChair(); var half = new Chair(); session.Publish(node, half); CheckTrue(ReferenceEquals(half, session.FromNode(node, typeof(Chair)))); } public void ObjectPublishedInAnotherSession_IsNotHandedOut() { SaveConverters registry = TestConverters(); SaveNode node = EmptyChair(); var half = new Chair(); registry.CreateSession().Publish(node, half); CheckFalse(ReferenceEquals(half, registry.CreateSession().FromNode(node, typeof(Chair)))); } public void ObjectOfAnotherTypeThanIsAskedFor_Throws() { SaveSession session = Session(); SaveNode node = EmptyChair(); session.Publish(node, new Stamp()); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(Chair)); } public void Struct_IsIgnored() { SaveSession session = Session(); SaveNode node = EmptyChair(); session.Publish(node, new Point(1, 2)); CheckNotNull(session.FromNode(node, typeof(Chair)) as Chair); } public void Null_Throws() { CheckThrow(typeof(ArgumentNullException)); TestConverters().CreateSession().Publish(EmptyChair(), null); } } public class MaxDepthTests : TestFixture { public void Default_AllowsAMillionLevels() { CheckEqual(1 << 20, TestConverters().MaxDepth); } public void Set_IsWhatGetReturns() { SaveConverters registry = TestConverters(); registry.MaxDepth = 5000; CheckEqual(5000, registry.MaxDepth); } public void SetToOne_Works() { SaveConverters registry = TestConverters(); registry.MaxDepth = 1; CheckEqual(1, registry.MaxDepth); } public void SetToZero_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); TestConverters().MaxDepth = 0; } } public class ToNodeTests : TestFixture { public void GenericConverter_IsClosedOverTheTypeArgument() { SaveNode node = GenericSession().ToNode(new Box { Content = 7 }, typeof(Box)); CheckEqual("7", node.RequireEntry("content").Text); } public void GenericConverter_IsClosedOverEachTypeArgumentSeparately() { SaveSession session = GenericSession(); SaveNode number = session.ToNode(new Box { Content = 7 }, typeof(Box)); SaveNode text = session.ToNode(new Box { Content = "seven" }, typeof(Box)); CheckEqual("7", number.RequireEntry("content").Text); CheckEqual("seven", text.RequireEntry("content").Text); } public void ConverterWithItsParametersInAnotherOrder_SavesEachPartAsItsOwnType() { var value = new Pair { First = 1, Second = "one" }; SaveNode node = GenericSession().ToNode(value, typeof(Pair)); CheckEqual("1", node.RequireEntry("first").Text); CheckEqual("one", node.RequireEntry("second").Text); } public void GenericTypeInsideAGenericType_Saves() { var value = new Box> { Content = new Box { Content = new Point(1.5, 0) } }; SaveNode node = GenericSession().ToNode(value, typeof(Box>)); CheckEqual("0x3FF8000000000000", node.RequireEntry("content").RequireEntry("content") .RequireEntry("x").Text); } public void Array_SavesEachElementAsItsType() { SaveNode node = GenericSession().ToNode(new[] { 3, 1, 2 }, typeof(int[])); CheckEqual(new[] { "3", "1", "2" }, node.Items.Select(item => item.Text).ToArray()); } public void ArrayOfArrays_Saves() { var value = new[] { new[] { "a" }, new[] { "b", "c" } }; SaveNode node = GenericSession().ToNode(value, typeof(string[][])); CheckEqual("c", node.Items[1].Items[1].Text); } public void ExactConverter_WinsOverAGenericOneThatAlsoMatches() { SaveSession session = new SaveConverters(new SaveConverter[] { new StubConverter>() }, new[] { typeof(BoxConverter<>) }).CreateSession(); CheckEqual("stub", session.ToNode(new Box(), typeof(Box)).Text); CheckEqual("7", session.ToNode(new Box { Content = 7 }, typeof(Box)) .RequireEntry("content").Text); } public void ClosedConverter_IsMadeOnceAndReused() { SaveSession session = GenericSession(); CountedConverter.Made = 0; session.ToNode(new Counted(), typeof(Counted)); session.ToNode(new Counted(), typeof(Counted)); session.FromNode(SaveNode.OfText("counted"), typeof(Counted)); CheckEqual(1, CountedConverter.Made); } public void FamilyType_IsWrittenByTheFamilysConverter() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>) }) .CreateSession(); CheckEqual("Rex", session.ToNode(new Wolf { Name = "Rex" }, typeof(Wolf)).Text); } public void ExactConverter_WinsOverAFamily() { SaveSession session = new SaveConverters(new SaveConverter[] { new StubConverter() }, new[] { typeof(CreatureConverter<>) }).CreateSession(); CheckEqual("stub", session.ToNode(new Wolf { Name = "Rex" }, typeof(Wolf)).Text); CheckEqual("Rex", session.ToNode(new AlphaWolf { Name = "Rex" }, typeof(AlphaWolf)).Text); } public void NearestBaseClassWithAFamily_Wins() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(WolfConverter<>) }).CreateSession(); CheckEqual("Rex", session.ToNode(new Creature { Name = "Rex" }, typeof(Creature)).Text); CheckEqual("wolf Rex", session.ToNode(new Wolf { Name = "Rex" }, typeof(Wolf)).Text); CheckEqual("wolf Rex", session.ToNode(new AlphaWolf { Name = "Rex" }, typeof(AlphaWolf)).Text); } public void FamilyWhoseConstraintTheTypeBreaks_LeavesItToTheNextBaseClass() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(StructLabeledHerdConverter<,,>), typeof(HerdConverter<,>) }).CreateSession(); var value = new NameHerd { Lead = "Rex" }; CheckEqual("Rex", session.ToNode(value, typeof(NameHerd)).RequireEntry("lead").Text); } public void FamilyOfAGenericBase_WritesWithTheBasesArguments() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(HerdConverter<,>) }) .CreateSession(); CheckEqual("4",session.ToNode(new IntHerd { Lead = 4 }, typeof(IntHerd)).RequireEntry("lead").Text); } public void FamilyConverter_IsMadeOncePerType() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>) }) .CreateSession(); CreatureConverter.Made = 0; CreatureConverter.Made = 0; session.ToNode(new Wolf { Name = "Rex" }, typeof(Wolf)); session.ToNode(new Wolf { Name = "Fang" }, typeof(Wolf)); session.ToNode(new AlphaWolf { Name = "Rex" }, typeof(AlphaWolf)); CheckEqual(1, CreatureConverter.Made); CheckEqual(1, CreatureConverter.Made); } public void TypeOutsideEveryFamily_IsSavedAsItWouldBeWithoutFamilies() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(HerdConverter<,>) }).CreateSession(); CheckEqual("5", session.ToNode(5, typeof(int)).Text); CheckEqual("five", session.ToNode("five", typeof(string)).Text); CheckEqual("five", session.ToNode("five", typeof(string)).Text); } public static SaveSession WithInterfaceFamilies(params Type[] families) { return new SaveConverters(new SaveConverter[0], families).CreateSession(); } public void InterfaceFamilyType_IsWrittenByTheFamilysConverter() { SaveSession session = WithInterfaceFamilies(typeof(CarnivoreConverter<>)); CheckEqual("carnivore", session.ToNode(new Lynx(), typeof(Lynx)).Text); CheckEqual("carnivore", session.ToNode(new Orca(), typeof(Orca)).Text); } public void StructInAnInterfaceFamily_IsWrittenByTheFamilysConverter() { CheckEqual("aquatic", WithInterfaceFamilies(typeof(AquaticConverter<>)) .ToNode(new Sprat(), typeof(Sprat)).Text); } public void ExactConverter_WinsOverAnInterfaceFamily() { SaveSession session = new SaveConverters(new SaveConverter[] { new StubConverter() }, new[] { typeof(CarnivoreConverter<>) }).CreateSession(); CheckEqual("stub", session.ToNode(new Lynx(), typeof(Lynx)).Text); } public void ClassFamily_WinsOverAnInterfaceFamily() { SaveSession session = WithInterfaceFamilies(typeof(CarnivoreConverter<>), typeof(CreatureConverter<>)); CheckEqual("Griff", session.ToNode(new Gryphon { Name = "Griff" }, typeof(Gryphon)).Text); } public void MostDerivedInterfaceFamily_WinsWhateverTheRegistrationOrder() { CheckEqual("predator", WithInterfaceFamilies(typeof(CarnivoreConverter<>), typeof(PredatorConverter<>)).ToNode(new Orca(), typeof(Orca)).Text); CheckEqual("predator", WithInterfaceFamilies(typeof(PredatorConverter<>), typeof(CarnivoreConverter<>)).ToNode(new Orca(), typeof(Orca)).Text); CheckEqual("carnivore", WithInterfaceFamilies(typeof(PredatorConverter<>), typeof(CarnivoreConverter<>)).ToNode(new Lynx(), typeof(Lynx)).Text); } public void TypeInTwoUnrelatedInterfaceFamilies_ThrowsNamingBothConverters() { SaveSession session = WithInterfaceFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)); string message = null; try { session.ToNode(new Shark(), typeof(Shark)); } catch (InvalidOperationException exception) { message = exception.Message; } CheckTrue(message != null && message.Contains("CarnivoreConverter") && message.Contains("AquaticConverter")); } public void TypeInOnlyOneOfTwoUnrelatedInterfaceFamilies_IsSaved() { SaveSession session = WithInterfaceFamilies(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)); CheckEqual("aquatic", session.ToNode(new Otter(), typeof(Otter)).Text); } public void GenericInterfaceFamily_TakesItsTypeParameterFromTheInterface() { CheckEqual("vessel of Int32", WithInterfaceFamilies(typeof(VesselConverter<,>)) .ToNode(new Barrel(), typeof(Barrel)).Text); } public void TypeImplementingAGenericInterfaceTwice_Throws() { SaveSession session = WithInterfaceFamilies(typeof(VesselConverter<,>)); CheckThrow(typeof(InvalidOperationException)); session.ToNode(new Twin(), typeof(Twin)); } public void InterfaceFamilyOfAnInterfaceEverythingImplements_LeavesVellumsOwnTypesAlone() { SaveSession session = WithInterfaceFamilies(typeof(ComparableConverter<>)); CheckEqual("5", session.ToNode(5, typeof(int)).Text); CheckEqual("text", session.ToNode("text", typeof(string)).Text); CheckEqual("Monday", session.ToNode(DayOfWeek.Monday, typeof(DayOfWeek)).Text); CheckEqual("5", session.ToNode(new List { 5 }, typeof(List)).Items[0].Text); } public void GenericSubtypeOfAFamilyBase_Throws() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(HerdConverter<,>) }) .CreateSession(); CheckThrow(typeof(NotSupportedException)); session.ToNode(new LabeledHerd { Lead = "Rex" }, typeof(Herd)); } public void SubtypeOfAClassNoFamilySaves_Throws() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(HerdConverter<,>) }) .CreateSession(); CheckThrow(typeof(NotSupportedException)); session.ToNode(new Wolf { Name = "Rex" }, typeof(Creature)); } public void FamilyConstructorThatThrows_ThrowsItsOwnException() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(ThrowingCreatureConverter<>) }).CreateSession(); CheckThrow(typeof(FormatException)); session.ToNode(new Wolf(), typeof(Wolf)); } public static SaveConverters WithMarkedConverters() { return new SaveConverters(new SaveConverter[] { new PointConverter() }, new[] { typeof(MarkedBoxConverter<>), typeof(MarkedPairConverter<,>), typeof(MarkedHerdConverter<,>), typeof(CreatureConverter<>), }); } public static SaveSession MarkedSession() { return WithMarkedConverters().CreateSession(); } public void MarkedArgumentTheTypeCanSave_IsSaved() { SaveSession session = MarkedSession(); CheckEqual("marked", session.ToNode(new Box(), typeof(Box)).Text); CheckEqual("marked", session.ToNode(new Box(), typeof(Box)).Text); CheckEqual("marked", session.ToNode(new Box(), typeof(Box)).Text); CheckEqual("marked", session.ToNode(new Box(), typeof(Box)).Text); CheckEqual("marked", session.ToNode(new Box>(), typeof(Box>)).Text); } public void MarkedArgumentOnlyASubtypeCanSave_IsSaved() { CheckEqual("marked", MarkedSession().ToNode(new Box(), typeof(Box)).Text); } public void MarkedArgumentNothingSaves_Throws() { CheckThrow(typeof(InvalidOperationException)); MarkedSession().ToNode(new Box(), typeof(Box)); } public void MarkedArgumentNothingSaves_ThrowsEachTime() { SaveSession session = MarkedSession(); try { session.ToNode(new Box(), typeof(Box)); } catch (InvalidOperationException) { } CheckThrow(typeof(InvalidOperationException)); session.ToNode(new Box(), typeof(Box)); } public void MarkedArgumentThatIsItselfMadeOfSomethingNothingSaves_Throws() { CheckThrow(typeof(InvalidOperationException)); MarkedSession().ToNode(new Box>(), typeof(Box>)); } public void ParameterWithoutTheMark_AcceptsAnything() { SaveSession session = SessionWith(typeof(MarkedPairConverter<,>)); CheckEqual("marked", session.ToNode(new Pair(), typeof(Pair)).Text); } public void MarkedParameterOfAPairInEitherPlace_IsChecked() { SaveSession session = SessionWith(typeof(MarkedPairConverter<,>)); CheckThrow(typeof(InvalidOperationException)); session.ToNode(new Pair(), typeof(Pair)); } public void MarkedFamilyParameterTheTypeCanSave_IsSaved() { SaveSession session = MarkedSession(); CheckEqual("marked", session.ToNode(new IntHerd(), typeof(IntHerd)).Text); } public void MarkedFamilyParameterNothingSaves_Throws() { CheckThrow(typeof(InvalidOperationException)); MarkedSession().ToNode(new PlanktonHerd(), typeof(PlanktonHerd)); } public void TypeThatHoldsItselfThroughAMarkedFamilyParameter_IsSaved() { CheckEqual("marked", MarkedSession().ToNode(new Flock(), typeof(Flock)).Text); } public void NullOfAGenericType_IsNull() { CheckEqual(SaveNode.Kinds.Null, GenericSession().ToNode(null, typeof(Box)).Kind); CheckEqual(SaveNode.Kinds.Null, GenericSession().ToNode(null, typeof(int[])).Kind); } public void NullableOfAGenericStruct_UsesTheStructsConverter() { SaveSession session = GenericSession(); var value = new Pair { First = 1, Second = 2 }; CheckEqual(SaveNode.Kinds.Null, session.ToNode(null, typeof(Pair?)).Kind); CheckEqual("2", session.ToNode(value, typeof(Pair?)).RequireEntry("second").Text); } public void GenericTypeWithNoConverter_Throws() { CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(new Collection(), typeof(Collection)); } public void TypeArgumentThatCantBeSaved_Throws() { var value = new Box> { Content = new Collection() }; CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(value, typeof(Box>)); } public void ArrayWithADimensionNotStartingAtZero_Throws() { Array shifted = Array.CreateInstance(typeof(int), new[] { 1, 1 }, new[] { 1, 1 }); CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(shifted, typeof(int[,])); } public void ListOfConvertedValues_SavesEachWithItsConverter() { var value = new List> { new Box { Content = new Point(1.5, 0) } }; SaveNode node = GenericSession().ToNode(value, typeof(List>)); CheckEqual("0x3FF8000000000000", node.Items[0].RequireEntry("content").RequireEntry("x").Text); } public void ExactConverterForACollectionsItem_IsUsed() { SaveNode node = GenericSession().ToNode(new[] { new Point(1.5, 0) }, typeof(Point[])); CheckEqual("0x3FF8000000000000", node.Items[0].RequireEntry("x").Text); } public void TypeArgumentThatBreaksTheConvertersConstraint_Throws() { SaveSession session = SessionWith(typeof(StructBoxConverter<>)); CheckThrow(typeof(NotSupportedException)); session.ToNode(new Box(), typeof(Box)); } public void OpenGenericType_Throws() { CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(new Box(), typeof(Box<>)); } public void GenericChainFarDeeperThanTheStackAllows_Saves() { SaveNode node = GenericSession().ToNode(CellChain(5000), typeof(Cell)); CheckEqual(5000, DepthOf(node, "next")); } public void ConvertedValue_IsWhatItsConverterWrites() { SaveNode node = Session().ToNode(new Point(1.5, -2), typeof(Point)); CheckEqual("0x3FF8000000000000", node.RequireEntry("x").Text); CheckEqual("0xC000000000000000", node.RequireEntry("y").Text); } public void ConverterCanSaveAnotherConvertedType() { var value = new Waypoint { Name = "end", At = new Point(1.5, 0) }; SaveNode node = Session().ToNode(value, typeof(Waypoint)); CheckEqual("0x3FF8000000000000", node.RequireEntry("at").RequireEntry("x").Text); } public void NullReferenceOrNullable_IsNullWithoutAskingTheConverter() { CheckEqual(SaveNode.Kinds.Null, Session().ToNode(null, typeof(Waypoint)).Kind); CheckEqual(SaveNode.Kinds.Null, Session().ToNode(null, typeof(Point?)).Kind); } public void BuiltInType_IsSavedAsBuiltIn() { CheckEqual("5", Session().ToNode(5, typeof(int)).Text); } public void UnsupportedType_Throws() { CheckThrow(typeof(NotSupportedException)); Session().ToNode(new Collection(), typeof(Collection)); } public void ChainFarDeeperThanTheStackAllows_Saves() { SaveNode node = Session().ToNode(Chain(10000), typeof(Link)); CheckEqual(10000, DepthOf(node, "next")); } public void ChainFarDeeperThanTheStackAllows_SurvivesBeingWrittenAsLuaText() { SaveNode node = Session().ToNode(Chain(10000), typeof(Link)); CheckEqual(10000, DepthOf(ThroughLuaText(node), "next")); } public void ShortChains_Save() { SaveSession session = Session(); CheckEqual(1, DepthOf(session.ToNode(Chain(1), typeof(Link)), "next")); CheckEqual(2, DepthOf(session.ToNode(Chain(2), typeof(Link)), "next")); CheckEqual(3, DepthOf(session.ToNode(Chain(3), typeof(Link)), "next")); } public void FinishedSave_HoldsNoReferencesToNodesItShares() { var shared = new Link(); SaveNode list = Session().ToNode(new List { shared, shared }, typeof(List)); CheckEqual(SaveNode.Kinds.Table, list.Items[0].Kind); CheckTrue(ReferenceEquals(list.Items[0], list.Items[1])); } public void LaterSaveInASession_ReusesTheNodeOfAnObjectSavedEarlier() { var shared = new Link { Next = new Link() }; SaveSession session = Session(); SaveNode first = session.ToNode(new List { shared }, typeof(List)); SaveNode second = session.ToNode(new List { shared }, typeof(List)); CheckTrue(ReferenceEquals(first.Items[0], second.Items[0])); CheckEqual(SaveNode.Kinds.Table, first.Items[0].Kind); } public void ValueSharedDeepInsideTwoChains_IsOneNode() { var shared = new Link(); Link first = shared; Link second = shared; for (int i = 0; i < 30; ++i) { first = new Link { Next = first }; second = new Link { Next = second }; } SaveNode list = Session().ToNode(new List { first, second }, typeof(List)); CheckTrue(ReferenceEquals(Descend(list.Items[0], "next", 30), Descend(list.Items[1], "next", 30))); } public void DeepChainThatLoopsBack_ReferencesTheNodeNotACopy() { Link head = Chain(40); Link tail = head; while (tail.Next != null) { tail = tail.Next; } tail.Next = head; SaveNode node = Session().ToNode(head, typeof(Link)); CheckTrue(ReferenceEquals(node, Descend(node, "next", 40))); } public void ConverterThatMakesUpValuesForever_Throws() { SaveConverters registry = TestConverters(); registry.MaxDepth = 1000; SaveSession session = registry.CreateSession(); CheckThrow(typeof(InvalidOperationException)); session.ToNode(new Endless(), typeof(Endless)); } public void ChainWithinMaxDepth_Saves() { SaveConverters registry = TestConverters(); registry.MaxDepth = 1000; SaveSession session = registry.CreateSession(); CheckEqual(900, DepthOf(session.ToNode(Chain(900), typeof(Link)), "next")); } public void ChainBeyondMaxDepth_Throws() { SaveConverters registry = TestConverters(); registry.MaxDepth = 1000; SaveSession session = registry.CreateSession(); CheckThrow(typeof(InvalidOperationException)); session.ToNode(Chain(1100), typeof(Link)); } public void ChainsAroundTheInlineDepth_SaveEveryLink() { int inline = SaveSession.InlineDepth; foreach (int length in new[] { inline - 1, inline, inline + 1, 2 * inline, 3 * inline + 1 }) { CheckEqual(length, DepthOf(Session().ToNode(Chain(length), typeof(Link)), "next")); } } public void RingsAroundTheInlineDepth_CloseOnTheFirstNode() { int inline = SaveSession.InlineDepth; foreach (int length in new[] { 2, inline - 1, inline, inline + 1, 2 * inline + 3 }) { Link head = Chain(length); Link tail = head; while (tail.Next != null) { tail = tail.Next; } tail.Next = head; SaveNode node = Session().ToNode(head, typeof(Link)); CheckTrue(ReferenceEquals(node, Descend(node, "next", length))); } } public void ChainOfMaxDepthLevels_SavesWhetherOrNotItDefers() { foreach (int levels in new[] { 3, SaveSession.InlineDepth + 1, 2 * SaveSession.InlineDepth + 3 }) { SaveConverters registry = TestConverters(); registry.MaxDepth = levels; CheckEqual(levels, DepthOf(registry.CreateSession().ToNode(Chain(levels), typeof(Link)), "next")); } } public void ChainOneLevelBeyondAMaxDepthThatNeverDefers_Throws() { SaveConverters registry = TestConverters(); registry.MaxDepth = 3; CheckThrow(typeof(InvalidOperationException)); registry.CreateSession().ToNode(Chain(4), typeof(Link)); } public void ChainOneLevelBeyondAMaxDepthThatDefers_Throws() { SaveConverters registry = TestConverters(); registry.MaxDepth = 2 * SaveSession.InlineDepth + 3; CheckThrow(typeof(InvalidOperationException)); registry.CreateSession().ToNode(Chain(registry.MaxDepth + 1), typeof(Link)); } public void AfterAConverterThatMadeUpValuesForever_StillSaves() { SaveConverters registry = TestConverters(); registry.MaxDepth = 1000; SaveSession session = registry.CreateSession(); try { session.ToNode(new Endless(), typeof(Endless)); } catch (InvalidOperationException) { } CheckEqual(40, DepthOf(session.ToNode(Chain(40), typeof(Link)), "next")); } public void ValueContainingItself_IsANodeThatRefersToItself() { var loop = new Link(); loop.Next = loop; SaveNode node = Session().ToNode(loop, typeof(Link)); CheckEqual(SaveNode.Kinds.Ref, node.RequireEntry("next").Kind); CheckTrue(ReferenceEquals(node, node.RequireEntry("next").Resolved)); } public void TwoValuesContainingEachOther_EachReferToTheOther() { var first = new Link(); var second = new Link { Next = first }; first.Next = second; SaveNode node = Session().ToNode(first, typeof(Link)); SaveNode inner = node.RequireEntry("next"); CheckEqual(SaveNode.Kinds.Table, inner.Kind); CheckTrue(ReferenceEquals(node, inner.RequireEntry("next").Resolved)); } public void CycleThroughASharedValue_ReferencesTheNodeNotAnotherCopy() { var loop = new Link(); loop.Next = loop; SaveNode list = Session().ToNode(new List { loop, loop }, typeof(List)); CheckTrue(ReferenceEquals(list.Items[0], list.Items[1])); CheckTrue(ReferenceEquals(list.Items[0], list.Items[0].RequireEntry("next").Resolved)); } public void ConverterThatReturnsTheNodeOfAValueThatCyclesBack_Throws() { var loop = new Echo(); loop.Other = loop; CheckThrow(typeof(InvalidOperationException)); Session().ToNode(loop, typeof(Echo)); } public void Subtype_IsSavedByItsOwnConverterAndNamed() { SaveNode node = Session().ToNode(new Dog { Name = "Rex", Barks = true }, typeof(Animal)); CheckEqual("Vellum.UnitTests.Dog", node.RequireEntry("__type").Text); CheckEqual("Rex", node.RequireEntry("name").Text); CheckEqual("true", node.RequireEntry("barks").Text); } public void ExactType_IsNotNamed() { SaveNode node = Session().ToNode(new Animal { Name = "Rex" }, typeof(Animal)); CheckFalse(node.Entries.ContainsKey("__type")); } public void NullableOfTheExactType_IsNotNamed() { SaveNode node = Session().ToNode(new Hamster { Nickname = "Bun" }, typeof(Hamster?)); CheckFalse(node.Entries.ContainsKey("__type")); } public void SubtypeWhoseConverterWritesNoTable_IsWrapped() { SaveNode node = Session().ToNode(new Fish { Name = "Nemo" }, typeof(Animal)); CheckEqual("Vellum.UnitTests.Fish", node.RequireEntry("__type").Text); CheckEqual("Nemo", node.RequireEntry("__value").Text); CheckEqual(2, node.Entries.Count); } public void StructInAnInterface_IsSavedByItsOwnConverterAndNamed() { SaveNode node = Session().ToNode(new Hamster { Nickname = "Bun" }, typeof(IPet)); CheckEqual("Vellum.UnitTests.Hamster", node.RequireEntry("__type").Text); CheckEqual("Bun", node.RequireEntry("nickname").Text); } public void SubtypesInAList_AreEachNamed() { var value = new List { new Animal { Name = "Tom" }, new Dog { Name = "Rex" } }; SaveNode node = Session().ToNode(value, typeof(List)); CheckFalse(node.Items[0].Entries.ContainsKey("__type")); CheckEqual("Vellum.UnitTests.Dog", node.Items[1].RequireEntry("__type").Text); } public void ObjectSavedTwiceInOneCall_IsTheSameNodeInBothPlaces() { var waypoint = new Waypoint { Name = "home" }; SaveNode node = Session().ToNode(new List { waypoint, waypoint }, typeof(List)); CheckTrue(ReferenceEquals(node.Items[0], node.Items[1])); } public void TwoObjectsWithEqualContents_AreTwoNodes() { SaveNode node = Session().ToNode( new List { new Waypoint { Name = "home" }, new Waypoint { Name = "home" } }, typeof(List)); CheckFalse(ReferenceEquals(node.Items[0], node.Items[1])); } public void ListHeldTwice_IsTheSameNodeInBothPlaces() { var inner = new List { 1, 2 }; SaveNode node = Session().ToNode(new List> { inner, inner }, typeof(List>)); CheckTrue(ReferenceEquals(node.Items[0], node.Items[1])); } public void ClassSavedAsTextHeldTwice_IsTheSameNodeInBothPlaces() { var fish = new Fish { Name = "Nemo" }; SaveNode node = Session().ToNode(new List { fish, fish }, typeof(List)); CheckEqual(SaveNode.Kinds.Text, node.Items[0].Kind); CheckTrue(ReferenceEquals(node.Items[0], node.Items[1])); } public void SameStringHeldTwice_IsTheSameNodeInBothPlaces() { string text = new string('s', 4); SaveNode texts = Session().ToNode(new List { text, text }, typeof(List)); CheckTrue(ReferenceEquals(texts.Items[0], texts.Items[1])); } public void EqualStringsThatAreTwoObjects_AreTwoNodes() { SaveNode texts = Session().ToNode( new List { new string('s', 4), new string('s', 4) }, typeof(List)); CheckFalse(ReferenceEquals(texts.Items[0], texts.Items[1])); } public void EmptyString_IsNeverShared() { SaveNode texts = Session().ToNode(new List { "", "" }, typeof(List)); CheckFalse(ReferenceEquals(texts.Items[0], texts.Items[1])); } public void Structs_AreNeverShared() { var point = new Point(1, 2); SaveNode points = Session().ToNode(new List { point, point }, typeof(List)); CheckFalse(ReferenceEquals(points.Items[0], points.Items[1])); } public void EqualObjectsThatAreTwoObjects_AreTwoNodes() { SaveNode node = Session().ToNode( new List { new Stamp { Id = "a" }, new Stamp { Id = "a" } }, typeof(List)); CheckFalse(ReferenceEquals(node.Items[0], node.Items[1])); } public void SubtypeHeldTwice_IsTheSameTaggedNode() { var dog = new Dog { Name = "Rex" }; SaveNode node = Session().ToNode(new List { dog, dog }, typeof(List)); CheckTrue(ReferenceEquals(node.Items[0], node.Items[1])); CheckEqual("Vellum.UnitTests.Dog", node.Items[0].RequireEntry("__type").Text); } public void ObjectChangedBetweenSessions_IsSavedAgain() { SaveConverters registry = TestConverters(); var waypoint = new Waypoint { Name = "home" }; registry.CreateSession().ToNode(waypoint, typeof(Waypoint)); waypoint.Name = "away"; SaveNode saved = registry.CreateSession().ToNode(waypoint, typeof(Waypoint)); CheckEqual("away", saved.RequireEntry("name").Text); } public void CycleSavedTwiceInOneSession_IsTheSameNode() { SaveSession session = Session(); var loop = new Link(); loop.Next = loop; SaveNode first = session.ToNode(loop, typeof(Link)); CheckTrue(ReferenceEquals(first, session.ToNode(loop, typeof(Link)))); } public void AfterAFailedSaveInACycle_TheNextSaveIsUnaffected() { SaveSession session = Session(); var loop = new Echo(); loop.Other = loop; try { session.ToNode(loop, typeof(Echo)); } catch (InvalidOperationException) { } CheckEqual(SaveNode.Kinds.Table, session.ToNode(Chain(2), typeof(Link)).Kind); } public void NullInAnInterfaceARegisteredTypeImplements_IsNull() { CheckEqual(SaveNode.Kinds.Null, Session().ToNode(null, typeof(IPet)).Kind); } public void NullInATypeNothingRegisteredHolds_Throws() { CheckThrow(typeof(NotSupportedException)); Session().ToNode(null, typeof(IDisposable)); } public void SubtypeNothingConverts_Throws() { CheckThrow(typeof(NotSupportedException)); Session().ToNode(new Eel(), typeof(Animal)); } public void SubtypeInAnInterfaceNothingConverts_Throws() { CheckThrow(typeof(NotSupportedException)); Session().ToNode(new Stray(), typeof(IPet)); } public void BuiltInValueInAnObject_IsNamedWithItsKeyword() { SaveNode node = Session().ToNode(5, typeof(object)); CheckEqual("int", node.RequireEntry("__type").Text); CheckEqual("5", node.RequireEntry("__value").Text); } public void BuiltInValueInAnObject_RoundTrips() { SaveSession session = Session(); CheckEqual(5, session.FromNode(session.ToNode(5, typeof(object)), typeof(object))); CheckEqual("hi", session.FromNode(session.ToNode("hi", typeof(object)), typeof(object))); CheckEqual(Mood.Angry, session.FromNode(session.ToNode(Mood.Angry, typeof(object)), typeof(object))); } public void BuiltInTableValueInAnObject_RoundTrips() { SaveSession session = Session(); var value = new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc); CheckEqual(value, session.FromNode(session.ToNode(value, typeof(object)), typeof(object))); } public void BuiltInValueLoadedIntoAnIncompatibleType_Throws() { SaveSession session = Session(); SaveNode node = session.ToNode(5, typeof(object)); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(Animal)); } public void GenericSubtype_IsNamedWithItsArguments() { SaveNode node = GenericSession().ToNode(new Box { Content = 3 }, typeof(object)); CheckEqual("Vellum.UnitTests.Box", node.RequireEntry("__type").Text); CheckEqual("3", node.RequireEntry("content").Text); } public void GenericSubtypeOfGenericTypes_IsNamedWithEachArgument() { var value = new Box>>(); SaveNode node = GenericSession().ToNode(value, typeof(object)); CheckEqual("Vellum.UnitTests.Box>>", node.RequireEntry("__type").Text); } public void ArraySubtype_IsNamedAndSavedAsTheArray() { SaveNode node = Session().ToNode(new[] { new Dog { Name = "Rex" } }, typeof(Animal[])); CheckEqual("Vellum.UnitTests.Dog[]", node.RequireEntry("__type").Text); CheckEqual("Rex", node.RequireEntry("__value").Items[0].RequireEntry("name").Text); } public void GenericSubtypeNestedInAGenericType_IsNamedWithAPlus() { SaveSession session = SessionWith(typeof(SlotConverter<,>)); var value = new Holder.Slot { Outer = 1, Inner = "x" }; SaveNode node = session.ToNode(value, typeof(object)); CheckEqual("Vellum.UnitTests.Holder+Slot", node.RequireEntry("__type").Text); } public void GenericSubtypeWithAnArgumentNothingNames_Throws() { CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(new Box(), typeof(object)); } public void GenericSubtypeNoConverterHandles_Throws() { CheckThrow(typeof(NotSupportedException)); GenericSession().ToNode(new Collection(), typeof(object)); } public void GenericSubtypeWithAnEnumArgument_IsNamedWithTheEnum() { SaveNode node = GenericSession().ToNode(new Box { Content = Mood.Angry }, typeof(object)); CheckEqual("Vellum.UnitTests.Box", node.RequireEntry("__type").Text); CheckEqual("Angry", node.RequireEntry("content").Text); } public void GenericSubtypeOfAClosedConverter_IsNamedAndSaved() { SaveSession session = new SaveConverters( new SaveConverter[] { new PointConverter(), new StubConverter>() }).CreateSession(); SaveNode node = session.ToNode(new Box(), typeof(object)); CheckEqual("Vellum.UnitTests.Box", node.RequireEntry("__type").Text); CheckEqual("stub", node.RequireEntry("__value").Text); } public void SubtypeWhoseConverterWritesATypeEntry_Throws() { CheckThrow(typeof(InvalidOperationException)); Session().ToNode(new Tricky(), typeof(Animal)); } public void SubtypeWhoseConverterWritesAValueEntry_Throws() { CheckThrow(typeof(InvalidOperationException)); Session().ToNode(new Sneaky(), typeof(Animal)); } public void ExactTypeWhoseConverterWritesATypeEntry_IsLeftAlone() { SaveNode node = Session().ToNode(new Tricky(), typeof(Tricky)); CheckEqual("somewhere else", node.RequireEntry("__type").Text); } public void SubtypeChainFarDeeperThanTheStackAllows_SavesEveryLevelNamed() { SaveNode node = Session().ToNode(Mole.Chain(2000), typeof(Animal)); CheckEqual(2000, DepthOf(node, "inner")); CheckEqual("Vellum.UnitTests.Mole", Descend(node, "inner", 1500).RequireEntry("__type").Text); } public void FailureDeepInAChain_IsNotRememberedByTheSession() { Animal inner = new Sneaky(); for (int i = 0; i < 40; ++i) { inner = new Mole { Inner = inner }; } SaveSession session = Session(); try { session.ToNode(inner, typeof(Animal)); } catch (InvalidOperationException) { } CheckThrow(typeof(InvalidOperationException)); session.ToNode(inner, typeof(Animal)); } } public class FromNodeTests : TestFixture { public const int ManyItems = 1000; public static readonly SaveNode ManyTexts = TextList(ManyItems); public static readonly SaveConverters SharedConverters = TestConverters(); public static SaveNode TextList(int count) { SaveNode returnMe = SaveNode.NewList(); for (int i = 0; i < count; ++i) { returnMe.Items.Add(SaveNode.OfText("text" + i)); } return returnMe; } [AllocationTest] public void ManyStrings_AllocateOnlyTheirList() { object loaded = SharedConverters.CreateSession().FromNode(ManyTexts, typeof(List)); CheckAllocationsAtMost(maxBytesPerRun: ManyItems * IntPtr.Size + 4096); CheckEqual(ManyItems, ((List)loaded).Count); } public void ReferenceToANode_LoadsWhatTheNodeHolds() { SaveSession session = Session(); SaveNode reference = SaveNode.NewRef(); reference.Resolve(session.ToNode(new Point(1.5, -2), typeof(Point))); CheckEqual(new Point(1.5, -2), (Point)session.FromNode(reference, typeof(Point))); } public void GenericConverter_RoundTrips() { SaveSession session = GenericSession(); SaveNode node = session.ToNode(new Box { Content = "seven" }, typeof(Box)); CheckEqual("seven", ((Box)session.FromNode(node, typeof(Box))).Content); } public void FamilyType_RoundTrips() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(WolfConverter<>) }).CreateSession(); SaveNode node = session.ToNode(new AlphaWolf { Name = "Rex" }, typeof(AlphaWolf)); object loaded = session.FromNode(node, typeof(AlphaWolf)); CheckTrue(loaded.GetType() == typeof(AlphaWolf)); CheckEqual("Rex", ((AlphaWolf)loaded).Name); } public void SubtypeInAFamily_RoundTripsAsItsBaseClass() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(WolfConverter<>) }).CreateSession(); SaveNode node = session.ToNode(new AlphaWolf { Name = "Rex" }, typeof(Creature)); object loaded = session.FromNode(node, typeof(Creature)); CheckEqual(TypeNamesTests.Ns + "AlphaWolf", node.RequireEntry(SaveConverters.TypeKey).Text); CheckTrue(loaded.GetType() == typeof(AlphaWolf)); CheckEqual("Rex", ((Creature)loaded).Name); } public void SubtypeInAFamily_RoundTripsAsAnAbstractBaseClass() { SaveSession session = SessionWith(typeof(CreatureConverter<>)); SaveNode node = session.ToNode(new Mastiff { Name = "Rex" }, typeof(AbstractCreature)); object loaded = session.FromNode(node, typeof(AbstractCreature)); CheckTrue(loaded.GetType() == typeof(Mastiff)); CheckEqual("Rex", ((Creature)loaded).Name); } public void InterfaceFamilyType_RoundTripsAsItsInterface() { SaveSession session = SessionWith(typeof(CarnivoreConverter<>)); SaveNode node = session.ToNode(new Lynx(), typeof(ICarnivore)); CheckEqual(TypeNamesTests.Ns + "Lynx", node.RequireEntry(SaveConverters.TypeKey).Text); CheckTrue(session.FromNode(node, typeof(ICarnivore)).GetType() == typeof(Lynx)); } public void InterfaceFamilyStruct_RoundTripsAsItsInterface() { SaveSession session = SessionWith(typeof(AquaticConverter<>)); SaveNode node = session.ToNode(new Sprat(), typeof(IAquatic)); CheckTrue(session.FromNode(node, typeof(IAquatic)) is Sprat); } public void InterfaceFamilyType_RoundTripsAsAnAbstractClassImplementingIt() { SaveSession session = SessionWith(typeof(AquaticConverter<>)); SaveNode node = session.ToNode(new Cod(), typeof(AbstractAquatic)); CheckTrue(session.FromNode(node, typeof(AbstractAquatic)) is Cod); } public void InterfaceFamilyType_RoundTripsAsAnInterfaceExtendingItsInterface() { SaveSession session = SessionWith(typeof(CarnivoreConverter<>), typeof(PredatorConverter<>)); SaveNode node = session.ToNode(new Orca(), typeof(IPredator)); CheckTrue(session.FromNode(node, typeof(IPredator)) is Orca); } public void InterfaceFamilyType_RoundTripsAsObject() { SaveSession session = SessionWith(typeof(CarnivoreConverter<>)); SaveNode node = session.ToNode(new Lynx(), typeof(object)); CheckTrue(session.FromNode(node, typeof(object)) is Lynx); } public void NullHeldAsAnInterfaceAFamilySaves_StaysNull() { SaveSession session = SessionWith(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)); foreach (Type declared in new[] { typeof(ICarnivore), typeof(IAquatic), typeof(AbstractAquatic), typeof(object) }) { CheckNull(session.FromNode(session.ToNode(null, declared), declared)); } } public void SubtypeNamedInASaveThatDoesNotImplementTheDeclaredInterface_Throws() { SaveSession session = SessionWith(typeof(CarnivoreConverter<>), typeof(AquaticConverter<>)); SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText(TypeNamesTests.Ns + "Otter"); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(ICarnivore)); } public void SubtypeNamedInASaveThatNoFamilySaves_Throws() { SaveSession session = SessionWith(typeof(CreatureConverter<>)); SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText("System.IO.Path"); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(Creature)); } public void SubtypeNamedInASaveThatIsntDerivedFromTheDeclaredType_Throws() { SaveSession session = new SaveConverters(new SaveConverter[0], new[] { typeof(CreatureConverter<>), typeof(HerdConverter<,>) }).CreateSession(); SaveNode node = SaveNode.NewTable(); node.Entries[SaveConverters.TypeKey] = SaveNode.OfText(TypeNamesTests.Ns + "IntHerd"); CheckThrow(typeof(InvalidDataException)); session.FromNode(node, typeof(Creature)); } public void SubtypeInAFamilyOfAGenericBase_RoundTrips() { SaveSession session = SessionWith(typeof(HerdConverter<,>)); SaveNode node = session.ToNode(new NameHerd { Lead = "Rex" }, typeof(Herd)); object loaded = session.FromNode(node, typeof(Herd)); CheckTrue(loaded.GetType() == typeof(NameHerd)); CheckEqual("Rex", ((Herd)loaded).Lead); } public void SubtypeInAFamilyInsideAList_RoundTrips() { SaveSession session = SessionWith(typeof(CreatureConverter<>)); var creatures = new List { new Wolf { Name = "Rex" }, new Creature { Name = "Hen" } }; SaveNode node = session.ToNode(creatures, typeof(List)); var loaded = (List)session.FromNode(node, typeof(List)); CheckTrue(loaded[0].GetType() == typeof(Wolf)); CheckTrue(loaded[1].GetType() == typeof(Creature)); } public void NullAboveAFamilysBaseClass_RoundTripsAsNull() { SaveSession session = SessionWith(typeof(WolfConverter<>)); SaveNode node = session.ToNode(null, typeof(Creature)); CheckEqual(SaveNode.Kinds.Null, node.Kind); CheckNull(session.FromNode(node, typeof(Creature))); } public void NullOfAnAbstractClassInAFamily_RoundTripsAsNull() { SaveSession session = SessionWith(typeof(CreatureConverter<>)); SaveNode node = session.ToNode(null, typeof(AbstractCreature)); CheckEqual(SaveNode.Kinds.Null, node.Kind); CheckNull(session.FromNode(node, typeof(AbstractCreature))); } public void FamilyOfAGenericBase_RoundTrips() { SaveSession session = SessionWith(typeof(HerdConverter<,>)); SaveNode node = session.ToNode(new NameHerd { Lead = "Rex" }, typeof(NameHerd)); CheckEqual("Rex", ((NameHerd)session.FromNode(node, typeof(NameHerd))).Lead); } public void ConverterWithItsParametersInAnotherOrder_RoundTrips() { SaveSession session = GenericSession(); var value = new Pair { First = 1, Second = "one" }; SaveNode node = session.ToNode(value, typeof(Pair)); var loaded = (Pair)session.FromNode(node, typeof(Pair)); CheckEqual(1, loaded.First); CheckEqual("one", loaded.Second); } public void GenericTypeInsideAGenericType_RoundTrips() { SaveSession session = GenericSession(); var value = new Box> { Content = new Box { Content = new Point(1.5, -2) } }; SaveNode node = session.ToNode(value, typeof(Box>)); var loaded = (Box>)session.FromNode(node, typeof(Box>)); CheckEqual(-2.0, loaded.Content.Content.Y); } public void ArrayOfArrays_RoundTrips() { SaveSession session = GenericSession(); var value = new[] { new[] { "a" }, new string[0], new[] { "b", "c" } }; SaveNode node = session.ToNode(value, typeof(string[][])); var loaded = (string[][])session.FromNode(node, typeof(string[][])); CheckEqual(3, loaded.Length); CheckEqual(0, loaded[1].Length); CheckEqual(new[] { "b", "c" }, loaded[2]); } public void NullForAGenericReferenceType_IsNull() { CheckNull(GenericSession().FromNode(SaveNode.Null, typeof(Box))); CheckNull(GenericSession().FromNode(SaveNode.Null, typeof(int[]))); } public void NullForAGenericValueType_Throws() { CheckThrow(typeof(InvalidDataException)); GenericSession().FromNode(SaveNode.Null, typeof(Pair)); } public void NullForANullableGenericValueType_IsNull() { CheckNull(GenericSession().FromNode(SaveNode.Null, typeof(Pair?))); } public void GenericTypeWithNoConverter_Throws() { CheckThrow(typeof(NotSupportedException)); GenericSession().FromNode(SaveNode.NewList(), typeof(Collection)); } public void GenericTreeTooDeep_Throws() { SaveSession session = GenericSession(); SaveNode deep = session.ToNode(CellChain(SaveNode.MaxDepth), typeof(Cell)); SaveNode deeper = SaveNode.NewTable(); deeper.Entries["next"] = deep; CheckThrow(typeof(InvalidOperationException)); session.FromNode(deeper, typeof(Cell)); } public void ConvertedValues_RoundTrip() { SaveSession session = Session(); var value = new Waypoint { Name = "end", At = new Point(1.5, -2) }; SaveNode node = session.ToNode(value, typeof(Waypoint)); var loaded = (Waypoint)session.FromNode(node, typeof(Waypoint)); CheckEqual("end", loaded.Name); CheckEqual(1.5, loaded.At.X); CheckEqual(-2.0, loaded.At.Y); } public void NullForAReferenceOrNullable_IsNull() { CheckNull(Session().FromNode(SaveNode.Null, typeof(Waypoint))); CheckNull(Session().FromNode(SaveNode.Null, typeof(Point?))); } public void NullableWithAValue_IsReadByItsConverter() { SaveSession session = Session(); SaveNode node = session.ToNode(new Point(3, 4), typeof(Point)); CheckEqual(3.0, ((Point)session.FromNode(node, typeof(Point?))).X); } public void NullForAConvertedValueType_Throws() { CheckThrow(typeof(InvalidDataException)); Session().FromNode(SaveNode.Null, typeof(Point)); } public void BuiltInType_IsReadAsBuiltIn() { CheckEqual(5, Session().FromNode(SaveNode.OfText("5"), typeof(int))); } public void TreeTooDeep_Throws() { SaveSession session = Session(); SaveNode deep = session.ToNode(Chain(SaveNode.MaxDepth), typeof(Link)); SaveNode deeper = SaveNode.NewTable(); deeper.Entries["next"] = deep; CheckThrow(typeof(InvalidOperationException)); session.FromNode(deeper, typeof(Link)); } public static SaveNode WaypointNode(string name) { SaveNode at = SaveNode.NewTable(); at.Entries["x"] = SaveNode.OfText("1"); at.Entries["y"] = SaveNode.OfText("2"); SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["name"] = SaveNode.OfText(name); returnMe.Entries["at"] = at; return returnMe; } public static SaveNode SharedWaypointList() { SaveNode waypoint = WaypointNode("home"); SaveNode list = SaveNode.NewList(); list.Items.Add(waypoint); list.Items.Add(waypoint); return list; } public void NodeHeldTwice_LoadsAsOneObject() { var loaded = (List)Session().FromNode(SharedWaypointList(), typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); CheckEqual("home", loaded[0].Name); } public void TextNodeOfAClassHeldTwice_LoadsAsOneObject() { SaveNode text = SaveNode.OfText("Nemo"); SaveNode list = SaveNode.NewList(); list.Items.Add(text); list.Items.Add(text); var loaded = (List)Session().FromNode(list, typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); CheckEqual("Nemo", loaded[0].Name); } public void TwoTextNodesOfAClassWithEqualText_LoadAsTwoObjects() { SaveNode list = SaveNode.NewList(); list.Items.Add(SaveNode.OfText("Nemo")); list.Items.Add(SaveNode.OfText("Nemo")); var loaded = (List)Session().FromNode(list, typeof(List)); CheckFalse(ReferenceEquals(loaded[0], loaded[1])); } public void ClassSavedAsTextHeldTwice_IsOneObjectAfterASaveFile() { SaveSession session = Session(); var fish = new Fish { Name = "Nemo" }; SaveNode save = SaveNode.NewTable(); save.Entries["school"] = session.ToNode(new List { fish, fish }, typeof(List)); SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)); var loaded = (List)session.FromNode(read.RequireEntry("school"), typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); } public void TextNodeHeldTwiceAndLoadedAsStrings_LoadsOneString() { SaveNode text = SaveNode.OfText("same"); SaveNode list = SaveNode.NewList(); list.Items.Add(text); list.Items.Add(text); var loaded = (List)Session().FromNode(list, typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); } public void TwoTextNodesWithEqualText_LoadAsTwoStrings() { SaveNode list = SaveNode.NewList(); list.Items.Add(SaveNode.OfText(new string('s', 4))); list.Items.Add(SaveNode.OfText(new string('s', 4))); var loaded = (List)Session().FromNode(list, typeof(List)); CheckFalse(ReferenceEquals(loaded[0], loaded[1])); CheckEqual(loaded[0], loaded[1]); } public void TextNodeLoadedAsAStringThenAsAnotherType_LoadsEachAsItsOwnType() { SaveSession session = Session(); SaveNode text = SaveNode.OfText("5"); object asString = session.FromNode(text, typeof(string)); CheckEqual(5, session.FromNode(text, typeof(int))); CheckTrue(ReferenceEquals(asString, session.FromNode(text, typeof(string)))); } public void SameStringHeldTwiceAfterASaveFile_IsOneString() { SaveSession session = Session(); string text = new string('s', 4); SaveNode save = SaveNode.NewTable(); save.Entries["texts"] = session.ToNode(new List { text, text }, typeof(List)); SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)); var loaded = (List)session.FromNode(read.RequireEntry("texts"), typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); } public void TwoNodesWithEqualContents_LoadAsTwoObjects() { SaveNode list = SaveNode.NewList(); list.Items.Add(WaypointNode("home")); list.Items.Add(WaypointNode("home")); var loaded = (List)Session().FromNode(list, typeof(List)); CheckFalse(ReferenceEquals(loaded[0], loaded[1])); } public void ListNodeHeldTwice_LoadsAsOneList() { SaveNode inner = SaveNode.NewList(); inner.Items.Add(SaveNode.OfText("1")); SaveNode outer = SaveNode.NewList(); outer.Items.Add(inner); outer.Items.Add(inner); var loaded = (List>)Session().FromNode(outer, typeof(List>)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); } public void NodeOfAStructHeldTwice_LoadsAsACopyEachTime() { SaveNode point = new PointConverter().ToNode(new Point(1, 2), Session()); SaveNode list = SaveNode.NewList(); list.Items.Add(point); list.Items.Add(point); var loaded = (List)Session().FromNode(list, typeof(List)); CheckEqual(1.0, loaded[0].X); CheckEqual(1.0, loaded[1].X); } public void SubtypeNodeHeldTwice_LoadsAsOneObject() { SaveSession session = Session(); var dog = new Dog { Name = "Rex" }; SaveNode saved = session.ToNode(new List { dog, dog }, typeof(List)); var loaded = (List)session.FromNode(saved, typeof(List)); CheckTrue(ReferenceEquals(loaded[0], loaded[1])); CheckEqual(typeof(Dog), loaded[0].GetType()); } public void SeparateSessions_LoadSeparateObjects() { SaveNode waypoint = SharedWaypointList().Items[0]; CheckFalse(ReferenceEquals(Session().FromNode(waypoint, typeof(Waypoint)), Session().FromNode(waypoint, typeof(Waypoint)))); } public void FailedLoad_IsNotRememberedByTheSession() { SaveNode broken = SaveNode.NewTable(); broken.Entries["twin"] = SaveNode.OfText("not a chair"); SaveSession session = Session(); try { session.FromNode(broken, typeof(Chair)); } catch (InvalidDataException) { } CheckThrow(typeof(InvalidDataException)); session.FromNode(broken, typeof(Chair)); } public void FailedLoad_ForgetsObjectsLoadedBeforeIt() { SaveNode waypoint = SharedWaypointList().Items[0]; SaveNode broken = SaveNode.NewTable(); broken.Entries["twin"] = SaveNode.OfText("not a chair"); SaveSession session = Session(); object first = session.FromNode(waypoint, typeof(Waypoint)); try { session.FromNode(broken, typeof(Chair)); } catch (InvalidDataException) { } CheckFalse(ReferenceEquals(first, session.FromNode(waypoint, typeof(Waypoint)))); } public void NodeContainingItself_Throws() { SaveNode loop = SaveNode.NewTable(); loop.Entries["next"] = loop; CheckThrow(typeof(InvalidDataException)); Session().FromNode(loop, typeof(Link)); } public void AfterNodeContainingItself_TheNextLoadIsUnaffected() { SaveSession session = Session(); SaveNode loop = SaveNode.NewTable(); loop.Entries["next"] = loop; try { session.FromNode(loop, typeof(Link)); } catch (InvalidDataException) { } SaveNode last = SaveNode.NewTable(); last.Entries["next"] = SaveNode.Null; CheckNotNull(session.FromNode(last, typeof(Link)) as Link); } public void CycleThroughAConverterThatPublishes_LoadsAsACycle() { SaveNode chair = SaveNode.NewTable(); chair.Entries["twin"] = chair; var loaded = (Chair)Session().FromNode(chair, typeof(Chair)); CheckTrue(ReferenceEquals(loaded, loaded.Twin)); } public void TwoValuesContainingEachOther_LoadAsEachOthersTwin() { SaveNode first = SaveNode.NewTable(); SaveNode second = SaveNode.NewTable(); first.Entries["twin"] = second; second.Entries["twin"] = first; var loaded = (Chair)Session().FromNode(first, typeof(Chair)); CheckFalse(ReferenceEquals(loaded, loaded.Twin)); CheckTrue(ReferenceEquals(loaded, loaded.Twin.Twin)); } public void CycleSavedAndWrittenAsText_LoadsAsACycle() { SaveSession session = Session(); var chair = new Chair(); chair.Twin = chair; SaveNode save = SaveNode.NewTable(); save.Entries["chair"] = session.ToNode(chair, typeof(Chair)); SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)); var loaded = (Chair)session.FromNode(read.RequireEntry("chair"), typeof(Chair)); CheckTrue(ReferenceEquals(loaded, loaded.Twin)); } public void CycleThroughAConverterThatDoesNotPublish_ThrowsTellingWhatToDo() { SaveNode desk = SaveNode.NewTable(); desk.Entries["twin"] = desk; string message = null; try { Session().FromNode(desk, typeof(Desk)); } catch (InvalidDataException exception) { message = exception.Message; } CheckTrue(message != null && message.Contains("Publish")); } public void NullForAnInterfaceARegisteredTypeImplements_IsNull() { CheckNull(Session().FromNode(SaveNode.Null, typeof(IPet))); } public void NullForATypeNothingRegisteredHolds_Throws() { CheckThrow(typeof(InvalidDataException)); Session().FromNode(SaveNode.Null, typeof(IDisposable)); } public void NamedSubtype_LoadsAsItsType() { SaveSession session = Session(); SaveNode node = ThroughLuaText(session.ToNode(new Dog { Name = "Rex", Barks = true }, typeof(Animal))); var loaded = (Dog)session.FromNode(node, typeof(Animal)); CheckEqual("Rex", loaded.Name); CheckTrue(loaded.Barks); } public void WrappedSubtype_LoadsAsItsType() { SaveSession session = Session(); SaveNode node = ThroughLuaText(session.ToNode(new Fish { Name = "Nemo" }, typeof(Animal))); var loaded = (Fish)session.FromNode(node, typeof(Animal)); CheckEqual("Nemo", loaded.Name); } public void StructInAnInterface_LoadsAsItsType() { SaveSession session = Session(); SaveNode node = ThroughLuaText(session.ToNode(new Hamster { Nickname = "Bun" }, typeof(IPet))); var loaded = (Hamster)session.FromNode(node, typeof(IPet)); CheckEqual("Bun", loaded.Nickname); } public void SubtypesInAList_LoadAsTheirTypes() { SaveSession session = Session(); var value = new List { new Animal { Name = "Tom" }, new Dog { Name = "Rex" } }; SaveNode node = ThroughLuaText(session.ToNode(value, typeof(List))); var loaded = (List)session.FromNode(node, typeof(List)); CheckEqual(typeof(Animal), loaded[0].GetType()); CheckEqual(typeof(Dog), loaded[1].GetType()); } public void SubtypeSavedAsAnotherBase_LoadsWhenTheNewDeclaredTypeStillFits() { SaveSession session = Session(); SaveNode node = session.ToNode(new Dog { Name = "Rex" }, typeof(Animal)); CheckEqual(typeof(Dog), session.FromNode(node, typeof(object)).GetType()); } public void NamedSubtype_LeavesTheNodeAsItWas() { SaveSession session = Session(); SaveNode node = session.ToNode(new Fish { Name = "Nemo" }, typeof(Animal)); session.FromNode(node, typeof(Animal)); CheckEqual(2, node.Entries.Count); CheckEqual("Nemo", node.RequireEntry("__value").Text); } public static SaveNode Named(string name) { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["__type"] = SaveNode.OfText(name); return returnMe; } public void GenericSubtype_LoadsAsItsClosedTypeInConvertersThatNeverSavedIt() { var value = new Box> { Content = new Pair { First = 4, Second = "x" } }; SaveNode node = ThroughLuaText(GenericSession().ToNode(value, typeof(object))); var loaded = (Box>)GenericSession().FromNode(node, typeof(object)); CheckEqual(4, loaded.Content.First); CheckEqual("x", loaded.Content.Second); } public void GenericSubtypeWithAnEnumArgument_Loads() { var value = new Box { Content = Palette.Shade.Dark }; SaveNode node = ThroughLuaText(GenericSession().ToNode(value, typeof(object))); var loaded = (Box)GenericSession().FromNode(node, typeof(object)); CheckEqual(Palette.Shade.Dark, loaded.Content); } public void GenericSubtypeInAList_LoadsEachAsItsClosedType() { var value = new List { new Box { Content = 1 }, new Box { Content = "s" } }; SaveNode node = ThroughLuaText(GenericSession().ToNode(value, typeof(List))); var loaded = (List)GenericSession().FromNode(node, typeof(List)); CheckEqual(typeof(Box), loaded[0].GetType()); CheckEqual("s", ((Box)loaded[1]).Content); } public void ArraySubtype_LoadsAsTheArray() { SaveSession session = Session(); SaveNode node = ThroughLuaText(session.ToNode(new[] { new Dog { Name = "Rex" } }, typeof(Animal[]))); var loaded = (Animal[])session.FromNode(node, typeof(Animal[])); CheckEqual(typeof(Dog[]), loaded.GetType()); CheckEqual("Rex", loaded[0].Name); } public void GenericSubtypeNestedInAGenericType_Loads() { SaveSession session = SessionWith(typeof(SlotConverter<,>)); var value = new Holder.Slot { Outer = 1, Inner = "x" }; SaveNode node = ThroughLuaText(session.ToNode(value, typeof(object))); var loaded = (Holder.Slot)session.FromNode(node, typeof(object)); CheckEqual("x", loaded.Inner); } public void GenericNameWithAnArgumentNothingRegistered_Throws() { CheckThrow(typeof(InvalidDataException)); GenericSession().FromNode(Named("Vellum.UnitTests.Box"), typeof(object)); } public void GenericNameNoConverterHandles_Throws() { CheckThrow(typeof(InvalidDataException)); GenericSession().FromNode(Named("Vellum.UnitTests.Limited"), typeof(object)); } public void MalformedName_Throws() { CheckThrow(typeof(InvalidDataException)); GenericSession().FromNode(Named("Vellum.UnitTests.Box"), typeof(Box)); } public void NameNothingRegistered_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries["__type"] = SaveNode.OfText("System.IO.FileInfo"); CheckThrow(typeof(InvalidDataException)); Session().FromNode(node, typeof(object)); } public void NameOfATypeTheDeclaredTypeDoesntHold_Throws() { SaveNode node = Session().ToNode(new Hamster { Nickname = "Bun" }, typeof(IPet)); CheckThrow(typeof(InvalidDataException)); Session().FromNode(node, typeof(Animal)); } public void NameThatIsntText_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries["__type"] = SaveNode.NewList(); CheckThrow(typeof(InvalidDataException)); Session().FromNode(node, typeof(Animal)); } public void ValueBesideOtherEntries_Throws() { SaveNode node = Session().ToNode(new Fish { Name = "Nemo" }, typeof(Animal)); node.Entries["name"] = SaveNode.OfText("Nemo"); CheckThrow(typeof(InvalidDataException)); Session().FromNode(node, typeof(Animal)); } public void SubtypeChainTooDeep_Throws() { SaveSession session = Session(); SaveNode deep = session.ToNode(Mole.Chain(SaveNode.MaxDepth), typeof(Animal)); SaveNode deeper = SaveNode.NewTable(); deeper.Entries["__type"] = SaveNode.OfText("Vellum.UnitTests.Mole"); deeper.Entries["inner"] = deep; CheckThrow(typeof(InvalidOperationException)); session.FromNode(deeper, typeof(Animal)); } } } }