using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using UnitTestSharp; namespace Vellum.UnitTests { public class SavedValueTests : TestFixture { public enum Phase { Waiting, Running, } [Flags] public enum Access { Read = 1, Write = 2, } public static void InGermanCulture(System.Action action) { CultureInfo previous = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); try { action(); } finally { Thread.CurrentThread.CurrentCulture = previous; } } public static T RoundTrip(T value) { return (T)SavedValue.FromNode(SavedValue.ToNode(value, typeof(T)), typeof(T)); } public static long Bits(double value) { return BitConverter.DoubleToInt64Bits(value); } public class DoubleToNodeTests : TestFixture { public void WritesSixteenHexDigitsOfBits_WithTheDecimalAsAComment() { SaveNode node = SavedValue.DoubleToNode(1.5); CheckEqual("0x3FF8000000000000", node.Text); CheckEqual("1.5", node.Comment); } public void NegativeZero_KeepsItsSignBit() { CheckEqual("0x8000000000000000", SavedValue.DoubleToNode(-0.0).Text); } public void Comment_UsesAPointInAnyCulture() { InGermanCulture(() => CheckEqual("1.5", SavedValue.DoubleToNode(1.5).Comment)); } } public class DoubleFromNodeTests : TestFixture { public static readonly SaveNode OneAndAHalfAsHex = SaveNode.OfText("0x3ff8000000000000"); public static readonly SaveNode OneAndAHalfNode = SavedValue.DoubleToNode(1.5); public void Hex_IsReadBitForBit() { var values = new[] { 0.1, 1.0 / 3.0, 0.6822871999174, -0.0, double.Epsilon, double.MaxValue, double.MinValue, double.NaN, double.PositiveInfinity, double.NegativeInfinity, }; foreach (double value in values) { CheckEqual(Bits(value), Bits(SavedValue.DoubleFromNode(SavedValue.DoubleToNode(value)))); } } public void LowercaseHex_IsRead() { CheckEqual(1.5, SavedValue.DoubleFromNode(SaveNode.OfText("0x3ff8000000000000"))); } public void Decimal_IsReadAsAHandEdit() { InGermanCulture(() => CheckEqual(1.5, SavedValue.DoubleFromNode(SaveNode.OfText("1.5")))); } public void FewerThanSixteenDigits_AreTheLowBits() { CheckEqual(1L, Bits(SavedValue.DoubleFromNode(SaveNode.OfText("0x1")))); CheckEqual(0x3ff8L, Bits(SavedValue.DoubleFromNode(SaveNode.OfText("0X3Ff8")))); } public void LeadingZerosBeyondSixteenDigits_AreRead() { CheckEqual(1.5, SavedValue.DoubleFromNode(SaveNode.OfText("0x00000000000000003FF8000000000000"))); } public void SixteenDigitsWithTheTopBitSet_AreNegative() { CheckEqual(-1.5, SavedValue.DoubleFromNode(SaveNode.OfText("0xBFF8000000000000"))); } public void MoreThanSixteenSignificantDigits_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0x10000000000000000")); } public void NoDigitsAfterThePrefix_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0x")); } public void MalformedHex_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0xNOPE")); } public void BadDigitAfterGoodOnes_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0x3FF8G")); } public void WhitespaceInHex_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0x3FF8 ")); } public void SignInHex_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.OfText("0x-1")); } [AllocationTest] public void Hex_AllocatesNothing() { double value = SavedValue.DoubleFromNode(OneAndAHalfAsHex); CheckNoAllocations(); CheckEqual(1.5, value); } [AllocationTest] public void NodeMadeFromADouble_AllocatesNothing() { double value = SavedValue.DoubleFromNode(OneAndAHalfNode); CheckNoAllocations(); CheckEqual(1.5, value); } public void NodeMadeFromADouble_IsReadBitForBit() { foreach (long bits in new[] { long.MinValue, 1L, 0x7FEFFFFFFFFFFFFFL }) { double value = BitConverter.Int64BitsToDouble(bits); CheckEqual(bits, Bits(SavedValue.DoubleFromNode(SavedValue.DoubleToNode(value)))); } } public void Table_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.DoubleFromNode(SaveNode.NewTable()); } } public static bool SameNode(SaveNode expected, SaveNode actual) { if (expected.Kind != actual.Kind || expected.Text != actual.Text || expected.Comment != actual.Comment) { return false; } if (expected.Kind != SaveNode.Kinds.Table) { return true; } if (expected.Entries.Count != actual.Entries.Count) { return false; } foreach (KeyValuePair entry in expected.Entries) { SaveNode other; if (!actual.Entries.TryGetValue(entry.Key, out other) || !SameNode(entry.Value, other)) { return false; } } return true; } /// /// Returns what the typed calls get wrong about compared with the general ones, or /// null if they agree on every one and round trip it. /// public static string LeafMismatch(params T[] values) { string name = typeof(T).Name; foreach (T value in values) { SaveNode typed; if (!SavedValue.TryToNode(value, out typed)) { return name + " " + value + " has no typed writer."; } if (!SameNode(SavedValue.ToNode(value, typeof(T)), typed)) { return name + " " + value + " is saved differently from the general path."; } T read; if (!SavedValue.TryFromNode(typed, out read)) { return name + " " + value + " has no typed reader."; } if (!object.Equals(SavedValue.FromNode(typed, typeof(T)), read)) { return name + " " + value + " is read differently from the general path."; } if (!object.Equals(value, read)) { return name + " " + value + " doesn't round trip."; } } return null; } public static string MessageOf(System.Action action) { try { action(); } catch (Exception exception) { return exception.GetType().Name + ": " + exception.Message; } return null; } public class TryToNodeTests : TestFixture { public static readonly Guid SomeGuid = new Guid("0f8fad5b-d9cb-469f-a165-70867728950e"); // Its decimal comment is nineteen characters, so a comment or text made up front would show in the budget. public static readonly double LongToWrite = 0.1 + 0.2; public void Integers_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(sbyte.MinValue, -1, 0, 1, sbyte.MaxValue)); CheckNull(LeafMismatch(0, 1, byte.MaxValue)); CheckNull(LeafMismatch(short.MinValue, -1, 0, short.MaxValue)); CheckNull(LeafMismatch(0, 1, ushort.MaxValue)); CheckNull(LeafMismatch(int.MinValue, -1, 0, 1, int.MaxValue)); CheckNull(LeafMismatch(0, 1, uint.MaxValue)); CheckNull(LeafMismatch(long.MinValue, -1, 0, long.MaxValue)); CheckNull(LeafMismatch(0, 1, ulong.MaxValue)); } public void Decimals_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(0m, -1.5m, 1.10m, 0.0000001m, decimal.MinValue, decimal.MaxValue)); } public void Booleans_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(true, false)); } public void Chars_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch('a', '\0', (char)0xD800, (char)0xE9, char.MaxValue)); } public void Doubles_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(0.0, -0.0, 1.5, 0.1, double.NaN, double.PositiveInfinity, double.NegativeInfinity, double.Epsilon, double.MinValue, double.MaxValue)); } public void Float_IsCommentedWithItsOwnShortestDigits() { SaveNode typed; SavedValue.TryToNode(0.1f, out typed); SaveNode general = SavedValue.ToNode(0.1f, typeof(float)); CheckEqual("0x3FB99999A0000000", typed.Text); CheckEqual("0.1", typed.Comment); CheckEqual("0.1", general.Comment); } public void Integers_AreNodesThatHoldTheNumber() { SaveNode typed; SavedValue.TryToNode(-5, out typed); long number; CheckTrue(typed.TryGetInteger(out number)); CheckEqual(-5L, number); CheckTrue(SavedValue.ToNode(-5, typeof(int)).TryGetInteger(out number)); CheckTrue(SavedValue.ToNode(ulong.MaxValue, typeof(ulong)).TryGetUnsigned(out ulong unsigned)); } public void Floats_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(0f, -0f, 1.5f, 0.1f, float.NaN, float.PositiveInfinity, float.Epsilon, float.MinValue, float.MaxValue)); } public void TimeSpans_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(TimeSpan.Zero, TimeSpan.FromSeconds(90.5), TimeSpan.MinValue, TimeSpan.MaxValue)); } public void DatesAndTimes_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(DateTime.MinValue, DateTime.MaxValue, new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc), new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Local))); CheckNull(LeafMismatch(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(2020, 1, 2, 3, 4, 5, TimeSpan.FromHours(-7)))); } public void Guids_SaveAsTheGeneralPathDoes() { CheckNull(LeafMismatch(Guid.Empty, SomeGuid)); } public void Strings_ReturnFalse() { SaveNode node; CheckFalse(SavedValue.TryToNode("text", out node)); CheckNull(node); } public void EnumsAndNullables_ReturnFalse() { SaveNode node; CheckFalse(SavedValue.TryToNode(Phase.Running, out node)); CheckFalse(SavedValue.TryToNode(5, out node)); } public void TypesThatAreNotBuiltIn_ReturnFalse() { SaveNode node; CheckFalse(SavedValue.TryToNode(new Point(1, 2), out node)); CheckFalse(SavedValue.TryToNode(new List(), out node)); CheckFalse(SavedValue.TryToNode(5, out node)); } [AllocationTest] public void Integer_AllocatesOnlyItsNode() { SaveNode node; SavedValue.TryToNode(12345, out node); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 44 : 80); CheckEqual("12345", node.Text); } [AllocationTest] public void Double_AllocatesOnlyItsNode() { SaveNode node; SavedValue.TryToNode(LongToWrite, out node); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 44 : 80); CheckEqual(LongToWrite, SavedValue.DoubleFromNode(node)); } } public class TryFromNodeTests : TestFixture { public static readonly SaveNode IntegerNode = SaveNode.OfText("12345"); public void Strings_ReturnFalse() { string value; CheckFalse(SavedValue.TryFromNode(SaveNode.OfText("text"), out value)); CheckNull(value); } public void EnumsAndNullables_ReturnFalse() { Phase phase; int? number; CheckFalse(SavedValue.TryFromNode(SaveNode.OfText("Running"), out phase)); CheckFalse(SavedValue.TryFromNode(SaveNode.OfText("5"), out number)); } public void TypesThatAreNotBuiltIn_ReturnFalse() { Point point; CheckFalse(SavedValue.TryFromNode(SaveNode.NewTable(), out point)); } public void NullNode_ReturnsFalseForTheGeneralPathToReport() { int value; CheckFalse(SavedValue.TryFromNode(SaveNode.Null, out value)); CheckEqual("InvalidDataException: A System.Int32 can't be null.", MessageOf(() => SavedValue.FromNode(SaveNode.Null, typeof(int)))); } public void ReferenceToText_ReadsWhatItStandsFor() { SaveNode reference = SaveNode.NewRef(); reference.Resolve(SaveNode.OfText("7")); int value; CheckTrue(SavedValue.TryFromNode(reference, out value)); CheckEqual(7, value); } public void ReferenceToNothing_Throws() { int value; CheckThrow(typeof(InvalidOperationException)); SavedValue.TryFromNode(SaveNode.NewRef(), out value); } public void TextThatIsNotAValue_ThrowsWhatTheGeneralPathThrows() { SaveNode text = SaveNode.OfText("seven"); string expected = MessageOf(() => SavedValue.FromNode(text, typeof(int))); string actual = MessageOf(() => { int value; SavedValue.TryFromNode(text, out value); }); CheckEqual(expected, actual); CheckTrue(actual.StartsWith("InvalidDataException: A saved System.Int32 isn't valid: ")); } public void NodeThatIsNotText_ThrowsWhatTheGeneralPathThrows() { SaveNode list = SaveNode.NewList(); string expected = MessageOf(() => SavedValue.FromNode(list, typeof(double))); string actual = MessageOf(() => { double value; SavedValue.TryFromNode(list, out value); }); CheckEqual(expected, actual); CheckTrue(actual.StartsWith("InvalidDataException: Expected saved text")); } public void NumberOutOfRange_Throws() { byte value; CheckThrow(typeof(InvalidDataException)); SavedValue.TryFromNode(SaveNode.OfText("256"), out value); } [AllocationTest] public void Integer_AllocatesNothing() { int value; SavedValue.TryFromNode(IntegerNode, out value); CheckNoAllocations(); CheckEqual(12345, value); } public static readonly SaveNode MadeFromAnInteger = SaveNode.OfInteger(12345); [AllocationTest] public void NodeMadeFromAnInteger_AllocatesNothing() { int value; SavedValue.TryFromNode(MadeFromAnInteger, out value); CheckNoAllocations(); CheckEqual(12345, value); } public void NodeMadeFromAnIntegerThatFitsAnotherType_ReadsAsThatType() { byte small; long wide; ulong unsigned; CheckTrue(SavedValue.TryFromNode(SaveNode.OfInteger(255), out small)); CheckTrue(SavedValue.TryFromNode(SaveNode.OfInteger(255), out wide)); CheckTrue(SavedValue.TryFromNode(SaveNode.OfInteger(255), out unsigned)); CheckEqual((byte)255, small); CheckEqual(255L, wide); CheckEqual(255UL, unsigned); } public void NodeMadeFromAnUnsignedThatFitsALong_ReadsAsALong() { long wide; CheckTrue(SavedValue.TryFromNode(SaveNode.OfUnsigned(5), out wide)); CheckEqual(5L, wide); } public void NodeMadeFromAnIntegerTooBigForTheType_Throws() { byte small; CheckThrow(typeof(InvalidDataException)); SavedValue.TryFromNode(SaveNode.OfInteger(256), out small); } public void NodeMadeFromANegativeIntegerReadAsUnsigned_Throws() { uint unsigned; CheckThrow(typeof(InvalidDataException)); SavedValue.TryFromNode(SaveNode.OfInteger(-1), out unsigned); } public void NodeMadeFromAnUnsignedTooBigForALong_Throws() { long wide; CheckThrow(typeof(InvalidDataException)); SavedValue.TryFromNode(SaveNode.OfUnsigned(ulong.MaxValue), out wide); } public void NodeMadeFromAnIntegerReadAsAnotherTypeThanItFits_ThrowsWhatTheTextWould() { SaveNode made = SaveNode.OfInteger(300); SaveNode text = SaveNode.OfText("300"); string fromText = MessageOf(() => SavedValue.FromNode(text, typeof(byte))); string fromNumber = MessageOf(() => SavedValue.FromNode(made, typeof(byte))); CheckEqual(fromText, fromNumber); CheckTrue(fromNumber.StartsWith("InvalidDataException: A saved System.Byte isn't valid: ")); } public void NodeMadeFromAnIntegerReadAsADouble_IsParsedFromItsText() { double value; CheckTrue(SavedValue.TryFromNode(SaveNode.OfInteger(5), out value)); CheckEqual(5.0, value); } } public class BuiltInTypesTests : TestFixture { public static readonly object[] Samples = { "text", true, 'c', (sbyte)-5, (byte)5, (short)-5, (ushort)5, -5, 5u, -5L, 5UL, 1.5f, 2.5, 3.5m, TimeSpan.FromTicks(7), new DateTime(2026, 10, 9, 1, 2, 3, DateTimeKind.Utc), new DateTimeOffset(2026, 10, 9, 1, 2, 3, TimeSpan.FromHours(2)), new Guid("0f8fad5b-d9cb-469f-a165-70867728950e"), }; public void HoldsExactlyTheEighteenTypesASampleIsGivenFor() { var sampled = new HashSet(); foreach (object sample in Samples) { sampled.Add(sample.GetType()); } CheckEqual(18, SavedValue.BuiltInTypes.Count); CheckTrue(sampled.SetEquals(SavedValue.BuiltInTypes)); } public void EachType_SavesAndLoadsItsSample() { foreach (object sample in Samples) { Type type = sample.GetType(); CheckTrue(sample.Equals(SavedValue.FromNode(SavedValue.ToNode(sample, type), type))); } } } public class IsBuiltInTests : TestFixture { public void EachBuiltInType_Is() { foreach (Type type in SavedValue.BuiltInTypes) { CheckTrue(SavedValue.IsBuiltIn(type)); } } public void Enum_Is() { CheckTrue(SavedValue.IsBuiltIn(typeof(Phase))); CheckTrue(SavedValue.IsBuiltIn(typeof(Access))); } public void NullableOfABuiltInTypeOrAnEnum_Is() { CheckTrue(SavedValue.IsBuiltIn(typeof(int?))); CheckTrue(SavedValue.IsBuiltIn(typeof(Guid?))); CheckTrue(SavedValue.IsBuiltIn(typeof(Phase?))); } public void OtherTypes_AreNot() { foreach (Type type in new[] { typeof(object), typeof(IntPtr), typeof(Version), typeof(int[]), typeof(List), typeof(Point), typeof(IntPtr?), }) { CheckFalse(SavedValue.IsBuiltIn(type)); } } } public class IsIntegerOrDecimalTests : TestFixture { public void IntegersAndDecimal_Are() { foreach (Type type in new[] { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(decimal), }) { CheckTrue(SavedValue.IsIntegerOrDecimal(type)); } } public void EnumsCharsAndFloatingPoint_AreNot() { foreach (Type type in new[] { typeof(Phase), typeof(char), typeof(double), typeof(bool) }) { CheckFalse(SavedValue.IsIntegerOrDecimal(type)); } } } public class ToNodeTests : TestFixture { public void NullStringOrNullable_IsNull() { CheckEqual(SaveNode.Kinds.Null, SavedValue.ToNode(null, typeof(string)).Kind); CheckEqual(SaveNode.Kinds.Null, SavedValue.ToNode(null, typeof(int?)).Kind); } public void NullableWithAValue_IsSavedLikeItsValue() { CheckEqual("7", SavedValue.ToNode((int?)7, typeof(int?)).Text); } public void Bool_IsLowercase() { CheckEqual("true", SavedValue.ToNode(true, typeof(bool)).Text); CheckEqual("false", SavedValue.ToNode(false, typeof(bool)).Text); } public void Enum_IsItsName() { CheckEqual("Running", SavedValue.ToNode(Phase.Running, typeof(Phase)).Text); CheckEqual("Read, Write", SavedValue.ToNode(Access.Read | Access.Write, typeof(Access)).Text); } public void Char_IsItsCode_WithTheCharacterAsAComment() { SaveNode node = SavedValue.ToNode('A', typeof(char)); CheckEqual("65", node.Text); CheckEqual("A", node.Comment); } public void Integer_IsPlainDigitsInAnyCulture() { InGermanCulture(() => CheckEqual("-1234567", SavedValue.ToNode(-1234567, typeof(int)).Text)); } public void Double_IsItsBits() { CheckEqual("0x3FF8000000000000", SavedValue.ToNode(1.5, typeof(double)).Text); } public void Float_IsSavedAsTheDoubleItWidensTo() { float value = 1.0f / 3.0f; CheckEqual(SavedValue.DoubleToNode(value).Text, SavedValue.ToNode(value, typeof(float)).Text); } public void TimeSpan_IsTicks_WithTheSpanAsAComment() { SaveNode node = SavedValue.ToNode(TimeSpan.FromTicks(15), typeof(TimeSpan)); CheckEqual("15", node.Text); CheckEqual("00:00:00.0000015", node.Comment); } public void DateTime_IsATableOfTicksAndKind() { SaveNode node = SavedValue.ToNode(new DateTime(ticks: 42, kind: DateTimeKind.Utc), typeof(DateTime)); CheckEqual("42", node.RequireEntry("ticks").Text); CheckEqual("Utc", node.RequireEntry("kind").Text); } public void DateTimeOffset_IsATableOfTicksAndOffset() { var value = new DateTimeOffset(ticks: 42 + TimeSpan.TicksPerDay, offset: TimeSpan.FromHours(2)); SaveNode node = SavedValue.ToNode(value, typeof(DateTimeOffset)); CheckEqual((42 + TimeSpan.TicksPerDay).ToString(CultureInfo.InvariantCulture), node.RequireEntry("ticks").Text); CheckEqual(TimeSpan.FromHours(2).Ticks.ToString(CultureInfo.InvariantCulture), node.RequireEntry("offset").Text); } public void Guid_IsItsHyphenatedForm() { var value = new Guid("01234567-89ab-cdef-0123-456789abcdef"); CheckEqual("01234567-89ab-cdef-0123-456789abcdef", SavedValue.ToNode(value, typeof(Guid)).Text); } public void UnsupportedType_Throws() { CheckThrow(typeof(NotSupportedException)); SavedValue.ToNode(new List(), typeof(List)); } public void NullOfAnUnsupportedType_StillThrows() { CheckThrow(typeof(NotSupportedException)); SavedValue.ToNode(null, typeof(List)); } } public class FromNodeTests : TestFixture { public void StringAndNullString_RoundTrip() { CheckEqual("a \"quoted\"\nline", RoundTrip("a \"quoted\"\nline")); CheckNull(RoundTrip(null)); } public void Nullable_RoundTripsNullAndValues() { CheckNull(RoundTrip(null)); CheckEqual(7, RoundTrip(7)); } public void Bool_RoundTrips() { CheckTrue(RoundTrip(true)); CheckFalse(RoundTrip(false)); } public void Bool_IsReadWhateverItsCaseAndPadding() { CheckTrue((bool)SavedValue.FromNode(SaveNode.OfText("True"), typeof(bool))); CheckTrue((bool)SavedValue.FromNode(SaveNode.OfText(" TRUE\n"), typeof(bool))); CheckFalse((bool)SavedValue.FromNode(SaveNode.OfText("False"), typeof(bool))); } public void Bool_OfOtherText_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.OfText("1"), typeof(bool)); } public void Double_RoundTripsBitForBit() { CheckEqual(Bits(-0.0), Bits(RoundTrip(-0.0))); } public void Enums_RoundTripCombinationsAndUndefinedValues() { CheckEqual(Access.Read | Access.Write, RoundTrip(Access.Read | Access.Write)); CheckEqual((Phase)99, RoundTrip((Phase)99)); } public void FlagsEnumOfZeroWithNoZeroMember_IsTheNumberZeroAndRoundTrips() { CheckEqual("0", SavedValue.ToNode((Access)0, typeof(Access)).Text); CheckEqual((Access)0, RoundTrip((Access)0)); } public void FlagsEnumWithBitsNoMemberHas_IsTheWholeNumberAndRoundTrips() { var value = Access.Read | (Access)8; CheckEqual("9", SavedValue.ToNode(value, typeof(Access)).Text); CheckEqual(value, RoundTrip(value)); } public void Char_RoundTripsEvenALoneSurrogate() { CheckEqual('A', RoundTrip('A')); CheckEqual((char)0xD800, RoundTrip((char)0xD800)); } public void EveryIntegerType_RoundTripsItsExtremes() { CheckEqual(sbyte.MinValue, RoundTrip(sbyte.MinValue)); CheckEqual(byte.MaxValue, RoundTrip(byte.MaxValue)); CheckEqual(short.MinValue, RoundTrip(short.MinValue)); CheckEqual(ushort.MaxValue, RoundTrip(ushort.MaxValue)); CheckEqual(int.MinValue, RoundTrip(int.MinValue)); CheckEqual(uint.MaxValue, RoundTrip(uint.MaxValue)); CheckEqual(long.MinValue, RoundTrip(long.MinValue)); CheckEqual(ulong.MaxValue, RoundTrip(ulong.MaxValue)); } public void Decimal_KeepsItsTrailingZeros() { CheckEqual("1.50", RoundTrip(1.50m).ToString(CultureInfo.InvariantCulture)); } public void Float_RoundTripsBitForBit() { foreach (float value in new[] { 1.0f / 3.0f, float.Epsilon, -0.0f, float.NaN, float.MaxValue }) { CheckEqual(Bits(value), Bits(RoundTrip(value))); } } public void TimeSpan_RoundTripsTicks() { CheckEqual(TimeSpan.FromTicks(123456789012345), RoundTrip(TimeSpan.FromTicks(123456789012345))); } public void DateTime_RoundTripsTicksAndKind() { foreach (DateTimeKind kind in new[] { DateTimeKind.Utc, DateTimeKind.Local, DateTimeKind.Unspecified }) { DateTime loaded = RoundTrip(new DateTime(ticks: 636000000000000001, kind: kind)); CheckEqual(636000000000000001, loaded.Ticks); CheckEqual(kind, loaded.Kind); } } public static SaveNode DateTimeNodeOfKind(string kind) { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["ticks"] = SaveNode.OfText("42"); returnMe.Entries["kind"] = SaveNode.OfText(kind); return returnMe; } public void DateTimeKindWrittenAsANumber_IsStillRead() { var loaded = (DateTime)SavedValue.FromNode(DateTimeNodeOfKind("1"), typeof(DateTime)); CheckEqual(DateTimeKind.Utc, loaded.Kind); } public void DateTimeKindWithTheWrongCase_IsRefused() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(DateTimeNodeOfKind("utc"), typeof(DateTime)); } public void DateTimeKindThatIsNotAName_IsRefused() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(DateTimeNodeOfKind("Sometime"), typeof(DateTime)); } public void DateTimeOffset_RoundTripsTicksAndOffset() { var value = new DateTimeOffset(ticks: 636000000000000001, offset: new TimeSpan(5, 30, 0)); DateTimeOffset loaded = RoundTrip(value); CheckEqual(value.Ticks, loaded.Ticks); CheckEqual(value.Offset, loaded.Offset); } public void Guid_RoundTrips() { var value = new Guid("01234567-89ab-cdef-0123-456789abcdef"); CheckEqual(value, RoundTrip(value)); } public void NullForANonNullableType_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.Null, typeof(int)); } public void TableWhereTextBelongs_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.NewTable(), typeof(int)); } public void TableMissingAnEntry_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries["ticks"] = SaveNode.OfText("42"); CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(node, typeof(DateTime)); } public void NumberOutOfRange_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.OfText("300"), typeof(byte)); } public void UnknownEnumName_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.OfText("Retired"), typeof(Phase)); } public void MalformedNumber_Throws() { CheckThrow(typeof(InvalidDataException)); SavedValue.FromNode(SaveNode.OfText("twelve"), typeof(int)); } public void UnsupportedType_Throws() { CheckThrow(typeof(NotSupportedException)); SavedValue.FromNode(SaveNode.OfText(""), typeof(List)); } } } }