using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using MoonSharp.Interpreter; using UnitTestSharp; namespace Vellum.UnitTests { public class LuaSaveFormatTests : TestFixture { public static SaveNode SampleSave() { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["zeta"] = SaveNode.OfText("last"); returnMe.Entries["alpha"] = SaveNode.OfText("first", comment: "the first"); returnMe.Entries["middle"] = SaveNode.Null; SaveNode when = SaveNode.NewTable(); when.Entries["ticks"] = SaveNode.OfText("5", comment: "five"); when.Entries["kind"] = SaveNode.OfText("Utc"); returnMe.Entries["when"] = when; returnMe.Entries["Run.waits"] = SaveNode.OfText("2"); return returnMe; } /// /// Returns tables, each holding the next as "a", the last holding "x". /// public static SaveNode NestedNodes(int levels) { SaveNode returnMe = SaveNode.OfText("x"); for (int i = 0; i < levels; ++i) { SaveNode outer = SaveNode.NewTable(); outer.Entries["a"] = returnMe; returnMe = outer; } return returnMe; } /// /// Returns Lua for tables, each holding the next as "a", the last holding "x". /// public static string NestedLua(int levels) { var builder = new StringBuilder(); builder.Insert(0, "{ a = ", levels).Append("'x'").Insert(builder.Length, " }", levels); return builder.ToString(); } /// /// Holds a stack size that MoonSharp runs out of within a few hundred levels of nesting. /// public const int SmallStackBytes = 256 * 1024; /// /// Holds the depth at which a test gives up waiting for a small stack to reject the nesting. /// public const int GiveUpDepth = 200000; public static string Repeated(string text, int count) { return string.Concat(Enumerable.Repeat(text, count)); } public static SaveNode ParseOnSmallStack(string text) { return LuaSaveFormat.Parse(text, LuaSaveFormat.DefaultMaximumInstructions, SmallStackBytes); } /// /// Returns the first depth at which the save makes is rejected for nesting too /// deeply on a small stack, having read every shallower one. Returns zero if none is rejected. /// public static int FirstDepthRejected(Func build) { int depth = 20; while (depth < GiveUpDepth) { try { ParseOnSmallStack(build(depth)); } catch (InvalidDataException exception) when (exception.Message.Contains("too many syntax levels")) { return depth; } depth = depth * 5 / 4 + 1; } return 0; } /// /// Returns a list of the given texts, with a null for each null. /// public static SaveNode ListOf(params string[] texts) { SaveNode returnMe = SaveNode.NewList(); foreach (string text in texts) { if (text == null) { returnMe.Items.Add(SaveNode.Null); } else { returnMe.Items.Add(SaveNode.OfText(text)); } } return returnMe; } /// /// Returns the save's Lua, where the only entry is under the key "k". /// public static string FormatHolding(SaveNode node) { SaveNode save = SaveNode.NewTable(); save.Entries["k"] = node; return LuaSaveFormat.Format(save); } /// /// Returns the start of a save that has shared tables. /// public static string SharedHeader(int count) { return "local shared = {}\n" + "for index = 1, " + count + " do shared[index] = {} end\n" + "local function fill(target, source)\n" + " for key, value in pairs(source) do target[key] = value end\n" + "end\n\n"; } public static SaveNode SaveOf(params KeyValuePair[] entries) { SaveNode returnMe = SaveNode.NewTable(); foreach (KeyValuePair entry in entries) { returnMe.Entries[entry.Key] = entry.Value; } return returnMe; } public static KeyValuePair Entry(string key, SaveNode value) { return new KeyValuePair(key, value); } /// /// Returns a list of one text whose line is exactly wide under the key "k". /// public static SaveNode ListOneLineWide(int width) { int overhead = FormatHolding(ListOf("x")).Split('\n')[1].Length - 1; return ListOf(new string('x', width - overhead)); } /// /// Returns lists, each holding the next, the last holding "x". /// public static SaveNode NestedListNodes(int levels) { SaveNode returnMe = ListOf("x"); for (int i = 1; i < levels; ++i) { SaveNode outer = SaveNode.NewList(); outer.Items.Add(returnMe); returnMe = outer; } return returnMe; } /// /// Returns Lua for lists, each holding the next, the last holding "x". /// public static string NestedListLua(int levels) { var builder = new StringBuilder(); builder.Insert(0, "{ ", levels).Append("'x'").Insert(builder.Length, " }", levels); return builder.ToString(); } /// /// Returns how many tables lie inside one another from , following "a". /// public static int TableDepth(SaveNode node) { int returnMe = 0; for (; node.Kind == SaveNode.Kinds.Table; node = node.Entries["a"]) { ++returnMe; } return returnMe; } /// /// Returns the table levels down from , following "a". /// public static SaveNode FollowTables(SaveNode node, int steps) { for (int i = 0; i < steps; ++i) { node = node.Entries["a"]; } return node; } /// /// Returns how many lists lie inside one another from , each holding the next first. /// public static int ListDepth(SaveNode node) { int returnMe = 0; for (; node.Kind == SaveNode.Kinds.List; node = node.Items[0]) { ++returnMe; } return returnMe; } public static KeyValuePair Pair(SaveNode key, SaveNode value) { return new KeyValuePair(key, value); } public static SaveNode MapOf(params KeyValuePair[] pairs) { SaveNode returnMe = SaveNode.NewMap(); returnMe.Pairs.AddRange(pairs); return returnMe; } /// /// Returns a table of the entries "x" and "y". /// public static SaveNode PointNode(string x, string y) { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries["x"] = SaveNode.OfText(x); returnMe.Entries["y"] = SaveNode.OfText(y); return returnMe; } /// /// Returns the message Parse fails with, or null if it doesn't fail. /// public static string ParseFailure(string lua) { try { LuaSaveFormat.Parse(lua); } catch (InvalidDataException exception) { return exception.Message; } return null; } public class QuoteTests : TestFixture { public void PlainText_IsWrappedInQuotes() { CheckEqual("\"abc\"", LuaSaveFormat.Quote("abc")); } public void BackslashAndQuote_AreEscaped() { CheckEqual("\"a\\\\b\\\"c\"", LuaSaveFormat.Quote("a\\b\"c")); } public void LineBreaks_AreEscaped() { CheckEqual("\"a\\nb\\rc\"", LuaSaveFormat.Quote("a\nb\rc")); } public void OtherControlCharacters_AreThreeDigitEscapes() { CheckEqual("\"\\0091\\000\\127\"", LuaSaveFormat.Quote("\t1\0\x7f")); } public void NonAscii_IsWrittenAsIs() { CheckEqual("\"é→\"", LuaSaveFormat.Quote("é→")); } public void EveryAsciiCharacter_ReadsBackIdentically() { var builder = new StringBuilder(); for (char character = '\0'; character < 128; ++character) { builder.Append(character); } string text = builder.ToString() + "é→"; CheckEqual(text, new Script().DoString("return " + LuaSaveFormat.Quote(text)).String); } } public class FormatKeyTests : TestFixture { public void Identifier_IsBare() { CheckEqual("delivered_Mwh2", LuaSaveFormat.FormatKey("delivered_Mwh2")); } public void Keyword_IsBracketed() { CheckEqual("[\"end\"]", LuaSaveFormat.FormatKey("end")); } public void LeadingDigit_IsBracketed() { CheckEqual("[\"2nd\"]", LuaSaveFormat.FormatKey("2nd")); } public void NonAsciiLetter_IsBracketed() { CheckEqual("[\"café\"]", LuaSaveFormat.FormatKey("café")); } public void Empty_IsBracketed() { CheckEqual("[\"\"]", LuaSaveFormat.FormatKey("")); } } public class FormatTests : TestFixture { public void WritesATableReturningEveryEntrySortedByName() { string expected = "return {\n" + " [\"Run.waits\"] = \"2\",\n" + " alpha = \"first\", -- the first\n" + " middle = null,\n" + " when = {\n" + " kind = \"Utc\",\n" + " ticks = \"5\", -- five\n" + " },\n" + " zeta = \"last\",\n" + "}\n"; CheckEqual(expected, LuaSaveFormat.Format(SampleSave())); } public void EmptyTable_ReturnsAnEmptyTable() { CheckEqual("return {\n}\n", LuaSaveFormat.Format(SaveNode.NewTable())); } public void NonTable_Throws() { CheckThrow(typeof(ArgumentException)); LuaSaveFormat.Format(SaveNode.OfText("loose")); } public void LineBreakInAComment_IsWrittenAsASpace() { SaveNode save = SaveNode.NewTable(); save.Entries["note"] = SaveNode.OfText("1", comment: "two\r\nlines"); CheckTrue(LuaSaveFormat.Format(save).Contains(" note = \"1\", -- two lines\n")); } public void TablesNestedAsDeepAsAllowed_AreWritten() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(SaveNode.MaxDepth - 1); CheckTrue(LuaSaveFormat.Format(save).Contains("a = \"x\",")); } public void TablesNestedUpToTheHoistDepth_StayInline() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(LuaSaveFormat.HoistDepth - 1); CheckFalse(LuaSaveFormat.Format(save).Contains(LuaSaveFormat.SharedName)); } public void TableNestedOneBeyondTheHoistDepth_MovesToTheSharedTable() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(LuaSaveFormat.HoistDepth); CheckTrue(LuaSaveFormat.Format(save).Contains("fill(" + LuaSaveFormat.SharedName + "[1], {")); } public void TablesNestedFarBeyondTheHoistDepth_ReadBackToTheSameDepth() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(1000); CheckEqual(1000, TableDepth(LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).Entries["a"])); } public void ChainOfTenThousandTables_ReadsBackToTheSameDepth() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(10000); CheckEqual(10000, TableDepth(LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).Entries["a"])); } public void DeepSave_WritesTheSameTextWhenWrittenReadAndWrittenAgain() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedNodes(100); string written = LuaSaveFormat.Format(save); CheckEqual(written, LuaSaveFormat.Format(LuaSaveFormat.Parse(written))); } public void DeepChainThatLoopsBack_ReadsBackAsTheSameNode() { SaveNode head = NestedNodes(100); SaveNode tail = head; while (tail.Entries["a"].Kind == SaveNode.Kinds.Table) { tail = tail.Entries["a"]; } tail.Entries["a"] = head; SaveNode save = SaveNode.NewTable(); save.Entries["a"] = head; SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).Entries["a"]; CheckTrue(ReferenceEquals(read, FollowTables(read, 100))); } public void ValueSharedByTwoDeepChains_ReadsBackAsOneNode() { SaveNode shared = NestedNodes(3); SaveNode first = shared; SaveNode second = shared; for (int i = 0; i < 40; ++i) { SaveNode outerFirst = SaveNode.NewTable(); outerFirst.Entries["a"] = first; first = outerFirst; SaveNode outerSecond = SaveNode.NewTable(); outerSecond.Entries["a"] = second; second = outerSecond; } SaveNode save = SaveNode.NewTable(); save.Entries["first"] = first; save.Entries["second"] = second; SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)); CheckTrue(ReferenceEquals(FollowTables(read.Entries["first"], 40), FollowTables(read.Entries["second"], 40))); } public void NodeHeldOnce_IsWrittenInPlaceWithNoSharedTable() { CheckEqual("return {\n k = {\n x = \"1\",\n y = \"2\",\n },\n}\n", FormatHolding(PointNode("1", "2"))); } public void TableHeldTwice_IsWrittenOnceAndReferredToByNumber() { SaveNode point = PointNode("1", "2"); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n x = \"1\",\n y = \"2\",\n})\n" + "return {\n a = shared[1],\n b = shared[1],\n}\n", LuaSaveFormat.Format(SaveOf(Entry("a", point), Entry("b", point)))); } public void TextNodeHeldTwice_IsMadeOnceByLeafAndReferredToByNumber() { SaveNode text = SaveNode.OfText("t"); CheckEqual(SharedHeader(1) + "shared[1] = leaf(\"t\")\n" + "return {\n a = shared[1],\n b = shared[1],\n}\n", LuaSaveFormat.Format(SaveOf(Entry("a", text), Entry("b", text)))); } public void TextNodeHeldTwiceInAList_IsReferredToOnItsOwnLines() { SaveNode text = SaveNode.OfText("t"); SaveNode list = SaveNode.NewList(); list.Items.Add(text); list.Items.Add(text); CheckEqual(SharedHeader(1) + "shared[1] = leaf(\"t\")\n" + "return {\n k = {\n shared[1],\n shared[1],\n },\n}\n", FormatHolding(list)); } public void TextAndTablesHeldTwice_ShareOneNumberingWithTextMadeFirst() { SaveNode text = SaveNode.OfText("t"); SaveNode point = PointNode("1", "2"); string written = LuaSaveFormat.Format(SaveOf(Entry("a", point), Entry("b", text), Entry("c", point), Entry("d", text))); CheckEqual(SharedHeader(2) + "shared[2] = leaf(\"t\")\n" + "fill(shared[1], {\n x = \"1\",\n y = \"2\",\n})\n" + "return {\n a = shared[1],\n b = shared[2],\n c = shared[1],\n d = shared[2],\n}\n", written); } public void TwoTextNodesWithEqualText_AreNotShared() { CheckFalse(LuaSaveFormat.Format(SaveOf(Entry("a", SaveNode.OfText("t")), Entry("b", SaveNode.OfText("t")))).Contains("shared")); } public void NullHeldTwice_IsWrittenInPlaceBothTimes() { CheckEqual("return {\n a = null,\n b = null,\n}\n", LuaSaveFormat.Format(SaveOf(Entry("a", SaveNode.Null), Entry("b", SaveNode.Null)))); } public void TextHeldTwiceAsMapKeys_IsNotShared() { SaveNode key = SaveNode.OfText("k"); SaveNode map = MapOf(Pair(key, SaveNode.OfText("1")), Pair(key, SaveNode.OfText("2"))); CheckFalse(FormatHolding(map).Contains("shared")); } public void TablesWithEqualContentsButTwoNodes_AreNotShared() { CheckFalse(LuaSaveFormat.Format(SaveOf(Entry("a", PointNode("1", "2")), Entry("b", PointNode("1", "2")))).Contains("shared")); } public void ListHeldTwice_IsWrittenOneItemToALine() { SaveNode list = ListOf("a", "b"); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n \"a\",\n \"b\",\n})\n" + "return {\n a = shared[1],\n b = shared[1],\n}\n", LuaSaveFormat.Format(SaveOf(Entry("a", list), Entry("b", list)))); } public void EmptyListHeldTwice_IsShared() { SaveNode list = SaveNode.NewList(); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n})\n" + "return {\n a = shared[1],\n b = shared[1],\n}\n", LuaSaveFormat.Format(SaveOf(Entry("a", list), Entry("b", list)))); } public void ListItemHeldTwice_IsReferredToWithoutAKey() { SaveNode point = PointNode("1", "2"); SaveNode list = SaveNode.NewList(); list.Items.Add(point); list.Items.Add(point); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n x = \"1\",\n y = \"2\",\n})\n" + "return {\n k = {\n shared[1],\n shared[1],\n },\n}\n", FormatHolding(list)); } public void MapValueHeldTwice_IsReferredTo() { SaveNode point = PointNode("1", "2"); SaveNode map = MapOf(Pair(SaveNode.OfText("a"), point), Pair(SaveNode.OfText("b"), point)); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n x = \"1\",\n y = \"2\",\n})\n" + "return {\n k = {\n a = shared[1],\n b = shared[1],\n },\n}\n", FormatHolding(map)); } public void SharedNodes_AreNumberedInTheOrderTheyAreFirstReferredTo() { SaveNode first = PointNode("1", "1"); SaveNode second = PointNode("2", "2"); string text = LuaSaveFormat.Format(SaveOf(Entry("a", second), Entry("b", first), Entry("c", first), Entry("d", second))); CheckEqual(SharedHeader(2) + "fill(shared[1], {\n x = \"2\",\n y = \"2\",\n})\n" + "fill(shared[2], {\n x = \"1\",\n y = \"1\",\n})\n" + "return {\n a = shared[1],\n b = shared[2],\n c = shared[2],\n d = shared[1],\n}\n", text); } public void SharedNodeInsideAnotherSharedNode_IsReferredToFromItsBody() { SaveNode inner = PointNode("1", "2"); SaveNode outer = SaveNode.NewTable(); outer.Entries["inner"] = inner; string text = LuaSaveFormat.Format(SaveOf(Entry("a", outer), Entry("b", outer), Entry("c", inner))); CheckEqual(SharedHeader(2) + "fill(shared[1], {\n inner = shared[2],\n})\n" + "fill(shared[2], {\n x = \"1\",\n y = \"2\",\n})\n" + "return {\n a = shared[1],\n b = shared[1],\n c = shared[2],\n}\n", text); } public void NodeInsideAKey_IsWrittenWholeAndDoesNotCountAsHeld() { SaveNode point = PointNode("1", "2"); SaveNode map = MapOf(Pair(point, SaveNode.OfText("v"))); string text = LuaSaveFormat.Format(SaveOf(Entry("m", map), Entry("p", point))); CheckFalse(text.Contains("shared")); CheckTrue(text.Contains("[{ x = \"1\", y = \"2\" }] = \"v\",")); } public void NodeHeldTwiceAndInsideAKey_IsSharedAndWrittenWholeInTheKey() { SaveNode point = PointNode("1", "2"); SaveNode map = MapOf(Pair(point, SaveNode.OfText("v"))); string text = LuaSaveFormat.Format(SaveOf(Entry("m", map), Entry("p", point), Entry("q", point))); CheckTrue(text.Contains("p = shared[1],")); CheckTrue(text.Contains("[{ x = \"1\", y = \"2\" }] = \"v\",")); } public void RefToTheSave_IsWrittenAsTheSaveHoldingItself() { SaveNode save = SaveNode.NewTable(); SaveNode reference = SaveNode.NewRef(); save.Entries["self"] = reference; reference.Resolve(save); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n self = shared[1],\n})\nreturn shared[1]\n", LuaSaveFormat.Format(save)); } public void RefToATableHeldElsewhere_IsWrittenLikeSharingTheTable() { SaveNode point = PointNode("1", "2"); SaveNode reference = SaveNode.NewRef(); reference.Resolve(point); CheckEqual(LuaSaveFormat.Format(SaveOf(Entry("a", point), Entry("b", point))), LuaSaveFormat.Format(SaveOf(Entry("a", point), Entry("b", reference)))); } public void RefsInAListAndAMap_AreFollowedToTheirTargets() { SaveNode point = PointNode("1", "2"); SaveNode reference = SaveNode.NewRef(); reference.Resolve(point); SaveNode list = SaveNode.NewList(); list.Items.Add(reference); list.Items.Add(point); SaveNode map = MapOf(Pair(SaveNode.OfText("m"), reference)); string text = LuaSaveFormat.Format(SaveOf(Entry("l", list), Entry("m", map))); CheckTrue(text.Contains("l = {\n shared[1],\n shared[1],\n },")); CheckTrue(text.Contains("m = {\n m = shared[1],\n },")); } public void RefAsAMapKey_IsWrittenAsItsTarget() { SaveNode point = PointNode("1", "2"); SaveNode reference = SaveNode.NewRef(); reference.Resolve(point); string text = FormatHolding(MapOf(Pair(reference, SaveNode.OfText("v")))); CheckTrue(text.Contains("[{ x = \"1\", y = \"2\" }] = \"v\",")); } public void RefsToTextInAList_ShareALine() { SaveNode first = SaveNode.NewRef(); SaveNode second = SaveNode.NewRef(); first.Resolve(SaveNode.OfText("a")); second.Resolve(SaveNode.OfText("b")); SaveNode list = SaveNode.NewList(); list.Items.Add(first); list.Items.Add(second); CheckEqual("return {\n k = { \"a\", \"b\" },\n}\n", FormatHolding(list)); } public void RefNeverResolved_Throws() { CheckThrow(typeof(InvalidOperationException)); FormatHolding(SaveNode.NewRef()); } public void TableInsideItself_IsWrittenAsASharedTableHoldingItself() { SaveNode save = SaveNode.NewTable(); save.Entries["self"] = save; CheckEqual(SharedHeader(1) + "fill(shared[1], {\n self = shared[1],\n})\nreturn shared[1]\n", LuaSaveFormat.Format(save)); } public void List_IsWrittenInItsOwnOrder() { CheckEqual("return {\n k = { \"b\", \"a\" },\n}\n", FormatHolding(ListOf("b", "a"))); } public void OneItem_SharesALineWithTheKey() { CheckEqual("return {\n k = { \"a\" },\n}\n", FormatHolding(ListOf("a"))); } public void TwoItems_ShareALine() { CheckEqual("return {\n k = { \"a\", \"b\" },\n}\n", FormatHolding(ListOf("a", "b"))); } public void ThreeItems_ShareALine() { CheckEqual("return {\n k = { \"a\", \"b\", \"c\" },\n}\n", FormatHolding(ListOf("a", "b", "c"))); } public void FourItems_GetALineEach() { string expected = "return {\n" + " k = {\n" + " \"a\",\n" + " \"b\",\n" + " \"c\",\n" + " \"d\",\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(ListOf("a", "b", "c", "d"))); } public void EmptyList_IsWrittenAsBraces() { CheckEqual("return {\n k = {},\n}\n", FormatHolding(SaveNode.NewList())); } public void NullItem_IsWrittenAsTheNullMarker() { CheckEqual("return {\n k = { \"a\", null },\n}\n", FormatHolding(ListOf("a", null))); } public void LineExactlyAsWideAsAllowed_StaysOnOneLine() { string line = FormatHolding(ListOneLineWide(LuaSaveFormat.LineWidth)).Split('\n')[1]; CheckEqual(LuaSaveFormat.LineWidth, line.Length); CheckTrue(line.StartsWith(" k = { \"")); } public void LineOneColumnTooWide_GetsALinePerItem() { string formatted = FormatHolding(ListOneLineWide(LuaSaveFormat.LineWidth + 1)); CheckTrue(formatted.StartsWith("return {\n k = {\n \"")); CheckTrue(formatted.EndsWith("\",\n },\n}\n")); } public void ListThatFitsAtOneDepth_GetsALinePerItemOneDeeper() { SaveNode list = ListOneLineWide(LuaSaveFormat.LineWidth); SaveNode inner = SaveNode.NewTable(); inner.Entries["k"] = list; SaveNode save = SaveNode.NewTable(); save.Entries["outer"] = inner; CheckTrue(LuaSaveFormat.Format(save).Contains("outer = {\n k = {\n \"")); } public void CommentedItem_GetsALineEachAndKeepsItsComment() { SaveNode list = SaveNode.NewList(); list.Items.Add(SaveNode.OfText("a", comment: "note")); list.Items.Add(SaveNode.OfText("b")); string expected = "return {\n" + " k = {\n" + " \"a\", -- note\n" + " \"b\",\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void DoubleItems_GetALineEachAndTheirDecimalAsAComment() { SaveNode list = SaveNode.NewList(); list.Items.Add(SavedValue.DoubleToNode(1.5)); list.Items.Add(SavedValue.DoubleToNode(-0.1)); string expected = "return {\n" + " k = {\n" + " \"0x3FF8000000000000\", -- 1.5\n" + " \"0xBFB999999999999A\", -- -0.1\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void DoubleEntry_IsWrittenWithItsDecimalAsAComment() { SaveNode save = SaveNode.NewTable(); save.Entries["speed"] = SavedValue.DoubleToNode(2.25); CheckEqual("return {\n speed = \"0x4002000000000000\", -- 2.25\n}\n", LuaSaveFormat.Format(save)); } public void IntegersBeyondWhatLuaNumbersHold_AreWrittenAsStringsAndReadBackExactly() { SaveNode save = SaveNode.NewTable(); save.Entries["high"] = SavedValue.ToNode(long.MaxValue, typeof(long)); save.Entries["low"] = SavedValue.ToNode(long.MinValue, typeof(long)); save.Entries["unsigned"] = SavedValue.ToNode(ulong.MaxValue, typeof(ulong)); string text = LuaSaveFormat.Format(save); SaveNode read = LuaSaveFormat.Parse(text); CheckTrue(text.Contains("high = \"9223372036854775807\",")); CheckTrue(text.Contains("low = \"-9223372036854775808\",")); CheckTrue(text.Contains("unsigned = \"18446744073709551615\",")); CheckEqual(long.MaxValue, SavedValue.FromNode(read.RequireEntry("high"), typeof(long))); CheckEqual(long.MinValue, SavedValue.FromNode(read.RequireEntry("low"), typeof(long))); CheckEqual(ulong.MaxValue, SavedValue.FromNode(read.RequireEntry("unsigned"), typeof(ulong))); } public void IntegerItems_AreWrittenJustAsTheirTextIs() { SaveNode numbers = SaveNode.NewList(); SaveNode texts = SaveNode.NewList(); foreach (long value in new[] { 0L, 7L, -12L, long.MaxValue }) { numbers.Items.Add(SaveNode.OfInteger(value)); texts.Items.Add(SaveNode.OfText(value.ToString(System.Globalization.CultureInfo.InvariantCulture))); } CheckEqual(FormatHolding(texts), FormatHolding(numbers)); CheckTrue(FormatHolding(numbers).Contains("\"9223372036854775807\"")); } public void IntegerEntry_IsWrittenAsAString() { SaveNode save = SaveNode.NewTable(); save.Entries["count"] = SaveNode.OfInteger(3); save.Entries["big"] = SaveNode.OfUnsigned(ulong.MaxValue); string text = LuaSaveFormat.Format(save); CheckTrue(text.Contains("count = \"3\",")); CheckTrue(text.Contains("big = \"18446744073709551615\",")); } public void FloatEntry_IsWrittenWithItsOwnDigitsAsTheComment() { SaveNode save = SaveNode.NewTable(); save.Entries["tenth"] = SaveNode.OfFloat(0.1f); CheckEqual("return {\n tenth = \"0x3FB99999A0000000\", -- 0.1\n}\n", LuaSaveFormat.Format(save)); } public void DoubleAsAMapKey_IsWrittenAsItsHex() { SaveNode map = SaveNode.NewMap(); map.Pairs.Add(new KeyValuePair(SavedValue.DoubleToNode(1.5), SaveNode.OfText("a"))); SaveNode save = SaveNode.NewTable(); save.Entries["m"] = map; string text = LuaSaveFormat.Format(save); CheckTrue(text.Contains("[\"0x3FF8000000000000\"] = \"a\"")); CheckEqual("a", LuaSaveFormat.Parse(text).RequireEntry("m").RequireEntry("0x3FF8000000000000").Text); } public void DoubleThroughLuaText_ReadsBackWithNoComment() { SaveNode save = SaveNode.NewTable(); save.Entries["speed"] = SavedValue.DoubleToNode(2.25); SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).RequireEntry("speed"); CheckFalse(read.HasComment); CheckEqual(2.25, SavedValue.DoubleFromNode(read)); } public void CommentedItemAfterPlainOnes_StillGetsALineEach() { SaveNode list = ListOf("a"); list.Items.Add(SaveNode.OfText("b", comment: "note")); string expected = "return {\n" + " k = {\n" + " \"a\",\n" + " \"b\", -- note\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void TableItem_GetsALineEach() { SaveNode list = SaveNode.NewList(); SaveNode table = SaveNode.NewTable(); table.Entries["a"] = SaveNode.OfText("1"); list.Items.Add(table); string expected = "return {\n" + " k = {\n" + " {\n" + " a = \"1\",\n" + " },\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void ListItem_GetsALineEachAndLaysItselfOut() { SaveNode list = SaveNode.NewList(); list.Items.Add(ListOf("a", "b")); list.Items.Add(ListOf("c", "d", "e", "f")); string expected = "return {\n" + " k = {\n" + " { \"a\", \"b\" },\n" + " {\n" + " \"c\",\n" + " \"d\",\n" + " \"e\",\n" + " \"f\",\n" + " },\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void ListNestedAsDeepAsAllowed_IsWritten() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedListNodes(SaveNode.MaxDepth - 1); CheckTrue(LuaSaveFormat.Format(save).Contains("\"x\"")); } public void ListsNestedFarBeyondTheHoistDepth_ReadBackToTheSameDepth() { SaveNode save = SaveNode.NewTable(); save.Entries["a"] = NestedListNodes(1000); CheckEqual(1000, ListDepth(LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).Entries["a"])); } public void MapValuesNestedFarBeyondTheHoistDepth_ReadBackToTheSameDepth() { SaveNode map = SaveNode.OfText("x"); for (int i = 0; i < 500; ++i) { map = MapOf(Pair(SaveNode.OfText("k"), map)); } SaveNode save = SaveNode.NewTable(); save.Entries["a"] = map; SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).Entries["a"]; int depth = 0; while (read.Kind != SaveNode.Kinds.Text) { read = read.RequireMap()[0].Value; ++depth; } CheckEqual(500, depth); } public void ListInsideItself_IsWrittenAsASharedListHoldingItself() { SaveNode list = SaveNode.NewList(); list.Items.Add(list); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n shared[1],\n})\n" + "return {\n k = shared[1],\n}\n", FormatHolding(list)); } public void ListAsTheSave_Throws() { CheckThrow(typeof(ArgumentException)); LuaSaveFormat.Format(ListOf("a")); } public void MapWithTextKeys_IsWrittenAsTheTableItReadsBackAs() { SaveNode map = MapOf(Pair(SaveNode.OfText("b"), SaveNode.OfText("1")), Pair(SaveNode.OfText("a"), SaveNode.OfText("2"))); string expected = "return {\n" + " k = {\n" + " a = \"2\",\n" + " b = \"1\",\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(map)); } public void MapWithTextKeys_BracketsAKeyThatIsALuaKeyword() { SaveNode map = MapOf(Pair(SaveNode.OfText("end"), SaveNode.OfText("1"))); CheckTrue(FormatHolding(map).Contains(" [\"end\"] = \"1\",")); } public void MapWithTableKeys_WritesEachKeyWholeInBracketsSortedByTheKeysText() { SaveNode map = MapOf(Pair(PointNode("3", "4"), SaveNode.OfText("mine")), Pair(PointNode("1", "2"), SaveNode.OfText("home"))); string expected = "return {\n" + " k = {\n" + " [{ x = \"1\", y = \"2\" }] = \"home\",\n" + " [{ x = \"3\", y = \"4\" }] = \"mine\",\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(map)); } public void MapPairsAddedInEitherOrder_AreWrittenIdentically() { SaveNode forward = MapOf(Pair(PointNode("1", "2"), SaveNode.Null), Pair(PointNode("3", "4"), SaveNode.Null)); SaveNode backward = MapOf(Pair(PointNode("3", "4"), SaveNode.Null), Pair(PointNode("1", "2"), SaveNode.Null)); CheckEqual(FormatHolding(forward), FormatHolding(backward)); } public void KeysThatShareAPrefix_SortByTheirText() { SaveNode map = MapOf(Pair(ListOf("a", "b"), SaveNode.Null), Pair(ListOf("a"), SaveNode.Null)); CheckTrue(FormatHolding(map).Contains("[{ \"a\" }] = null,\n [{ \"a\", \"b\" }] = null,")); } public void ListKey_IsWrittenOnOneLine() { SaveNode map = MapOf(Pair(ListOf("a", "b"), SaveNode.OfText("v"))); CheckTrue(FormatHolding(map).Contains(" [{ \"a\", \"b\" }] = \"v\",")); } public void KeyHoldingNestedTablesAndNulls_IsWrittenOnOneLine() { SaveNode inner = SaveNode.NewTable(); inner.Entries["b"] = SaveNode.OfText("1"); SaveNode key = SaveNode.NewTable(); key.Entries["a"] = inner; key.Entries["c"] = SaveNode.Null; key.Entries["d"] = ListOf("p", null); SaveNode map = MapOf(Pair(key, SaveNode.OfText("v"))); CheckTrue(FormatHolding(map).Contains( " [{ a = { b = \"1\" }, c = null, d = { \"p\", null } }] = \"v\",")); } public void EmptyKey_IsWrittenAsBraces() { SaveNode map = MapOf(Pair(SaveNode.NewTable(), SaveNode.OfText("v"))); CheckTrue(FormatHolding(map).Contains(" [{}] = \"v\",")); } public void KeyWiderThanTheLineLimit_StillStaysOnOneLine() { SaveNode map = MapOf(Pair(PointNode(new string('x', 200), "y"), SaveNode.OfText("v"))); string line = FormatHolding(map).Split('\n')[2]; CheckTrue(line.Length > LuaSaveFormat.LineWidth); CheckTrue(line.StartsWith(" [{ x = \"xxx")); CheckTrue(line.EndsWith("\", y = \"y\" }] = \"v\",")); } public void CommentsInsideAKey_AreLeftOut() { SaveNode key = SaveNode.NewTable(); key.Entries["x"] = SaveNode.OfText("1", comment: "note"); SaveNode map = MapOf(Pair(key, SaveNode.OfText("v"))); string formatted = FormatHolding(map); CheckTrue(formatted.Contains("[{ x = \"1\" }] = \"v\",")); CheckFalse(formatted.Contains("note")); } public void ValueOfATableKeyedEntry_IsLaidOutLikeAnyOther() { SaveNode map = MapOf(Pair(PointNode("1", "2"), ListOf("a", "b", "c", "d"))); string expected = "return {\n" + " k = {\n" + " [{ x = \"1\", y = \"2\" }] = {\n" + " \"a\",\n" + " \"b\",\n" + " \"c\",\n" + " \"d\",\n" + " },\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(map)); } public void EmptyMap_IsWrittenAsAnEmptyTable() { CheckEqual("return {\n k = {\n },\n}\n", FormatHolding(SaveNode.NewMap())); } public void MapAsAListItem_GetsALineEach() { SaveNode list = SaveNode.NewList(); list.Items.Add(MapOf(Pair(PointNode("1", "2"), SaveNode.OfText("v")))); string expected = "return {\n" + " k = {\n" + " {\n" + " [{ x = \"1\", y = \"2\" }] = \"v\",\n" + " },\n" + " },\n" + "}\n"; CheckEqual(expected, FormatHolding(list)); } public void MapWithANullKey_Throws() { SaveNode map = MapOf(Pair(SaveNode.Null, SaveNode.OfText("v"))); CheckThrow(typeof(InvalidOperationException)); FormatHolding(map); } public void MapMixingTextAndTableKeys_Throws() { SaveNode map = MapOf(Pair(SaveNode.OfText("a"), SaveNode.Null), Pair(PointNode("1", "2"), SaveNode.Null)); CheckThrow(typeof(InvalidOperationException)); FormatHolding(map); } public void MapInsideAKeyWithTextKeys_IsWrittenAsATable() { SaveNode inside = MapOf(Pair(SaveNode.OfText("a"), SaveNode.OfText("1"))); SaveNode map = MapOf(Pair(inside, SaveNode.OfText("v"))); CheckTrue(FormatHolding(map).Contains("[{ a = \"1\" }] = \"v\",")); } public void MapInsideAKeyWithTableKeys_Throws() { SaveNode inside = MapOf(Pair(PointNode("1", "2"), SaveNode.Null)); SaveNode map = MapOf(Pair(inside, SaveNode.OfText("v"))); CheckThrow(typeof(InvalidOperationException)); FormatHolding(map); } public void MapInsideAKeyWithANullKey_Throws() { SaveNode inside = MapOf(Pair(SaveNode.Null, SaveNode.OfText("1"))); SaveNode map = MapOf(Pair(inside, SaveNode.OfText("v"))); CheckThrow(typeof(InvalidOperationException)); FormatHolding(map); } public void KeyNestedAsDeepAsAllowed_IsWritten() { SaveNode map = MapOf(Pair(NestedNodes(SaveNode.MaxDepth - 2), SaveNode.OfText("v"))); CheckTrue(FormatHolding(map).Contains("}] = \"v\",")); } public void KeyNestedTooDeeply_Throws() { SaveNode map = MapOf(Pair(NestedNodes(SaveNode.MaxDepth - 1), SaveNode.OfText("v"))); CheckThrow(typeof(InvalidOperationException)); FormatHolding(map); } public void MapInsideItself_IsWrittenAsASharedMapHoldingItself() { SaveNode map = SaveNode.NewMap(); map.Pairs.Add(Pair(PointNode("1", "2"), map)); CheckEqual(SharedHeader(1) + "fill(shared[1], {\n [{ x = \"1\", y = \"2\" }] = shared[1],\n})\n" + "return {\n k = shared[1],\n}\n", FormatHolding(map)); } public void MapAsTheSave_Throws() { CheckThrow(typeof(ArgumentException)); LuaSaveFormat.Format(SaveNode.NewMap()); } } public class ParseTests : TestFixture { public void ReadsWhatFormatWrote() { SaveNode save = LuaSaveFormat.Parse(LuaSaveFormat.Format(SampleSave())); CheckEqual(5, save.Entries.Count); CheckEqual("first", save.Entries["alpha"].Text); CheckEqual(SaveNode.Kinds.Null, save.Entries["middle"].Kind); CheckEqual("5", save.Entries["when"].RequireEntry("ticks").Text); CheckEqual("Utc", save.Entries["when"].RequireEntry("kind").Text); CheckEqual("last", save.Entries["zeta"].Text); CheckEqual("2", save.Entries["Run.waits"].Text); } public void Comments_AreNotReadBack() { CheckNull(LuaSaveFormat.Parse(LuaSaveFormat.Format(SampleSave())).Entries["alpha"].Comment); } public void EmptyTable_IsAnEmptyTable() { SaveNode save = LuaSaveFormat.Parse("return {}"); CheckEqual(SaveNode.Kinds.Table, save.Kind); CheckEqual(0, save.Entries.Count); } public void ComputedValues_AreAllowed() { CheckEqual("act2.build", LuaSaveFormat.Parse("return { at = 'act2' .. '.build' }").Entries["at"].Text); } public void SyntaxError_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return {"); } public void RuntimeError_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("error('broken')"); } public void NonTable_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return 'act2'"); } public void FunctionValue_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { counter = function() end }"); } public void WholeNumber_ReadsAsItsDigits() { CheckEqual("12", LuaSaveFormat.Parse("return { counter = 12 }").Entries["counter"].Text); CheckEqual("-7", LuaSaveFormat.Parse("return { counter = -7 }").Entries["counter"].Text); CheckEqual("0", LuaSaveFormat.Parse("return { counter = 0 }").Entries["counter"].Text); } public void LargestExactWholeNumber_KeepsEveryDigit() { CheckEqual("9007199254740992", LuaSaveFormat.Parse("return { counter = 9007199254740992 }").Entries["counter"].Text); CheckEqual("-9007199254740992", LuaSaveFormat.Parse("return { counter = -9007199254740992 }").Entries["counter"].Text); } public void WholeNumberPastTwoToTheFiftyThird_NamesTheEntryAndSaysToQuoteIt() { string message = ParseFailure("return { k = { counter = 9007199254740994 } }"); CheckTrue(message != null && message.Contains("k.counter") && message.Contains("Put it in quotes")); } public void NegativeWholeNumberPastTwoToTheFiftyThird_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { counter = -9007199254740994 }"); } public void Infinity_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { x = 1e999 }"); } public void FractionalNumber_ReadsBackAsTheSameDouble() { CheckEqual(0.1, SavedValue.DoubleFromNode(LuaSaveFormat.Parse("return { x = 0.1 }").Entries["x"])); CheckEqual(-2.5, SavedValue.DoubleFromNode(LuaSaveFormat.Parse("return { x = -2.5 }").Entries["x"])); } public void Booleans_ReadAsTheirText() { SaveNode save = LuaSaveFormat.Parse("return { yes = true, no = false }"); CheckEqual("true", save.Entries["yes"].Text); CheckEqual("false", save.Entries["no"].Text); } public void NumbersAndBooleans_ReadInsideListsAndMaps() { SaveNode save = LuaSaveFormat.Parse("return { list = { 1, true }, map = { [{ a = 'x' }] = 2 } }"); CheckEqual("1", save.Entries["list"].Items[0].Text); CheckEqual("true", save.Entries["list"].Items[1].Text); CheckEqual("2", save.Entries["map"].Pairs[0].Value.Text); } public void SaveThatIsAList_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { 'positional' }"); } public void BooleanKey_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { [true] = 'yes' }"); } public void QuotedNull_IsAString() { CheckEqual("null", LuaSaveFormat.Parse("return { note = 'null' }").Entries["note"].Text); } public void NestedEmptyTable_IsAnEmptyTableNotNull() { SaveNode note = LuaSaveFormat.Parse("return { note = {} }").Entries["note"]; CheckEqual(SaveNode.Kinds.Table, note.Kind); CheckEqual(0, note.Entries.Count); } public void NestedFunctionValue_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { when = { ticks = function() end } }"); } public void NestedBooleanKey_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { when = { [true] = 'yes' } }"); } public void TablesNestedAsDeepAsAllowed_AreRead() { SaveNode save = LuaSaveFormat.Parse("return { a = " + NestedLua(SaveNode.MaxDepth - 1) + " }"); CheckEqual(SaveNode.Kinds.Table, save.Entries["a"].Kind); } public void TablesNestedFarBeyondTheWriterDepth_AreRead() { SaveNode save = LuaSaveFormat.Parse("return { a = " + NestedLua(500) + " }"); CheckEqual(500, TableDepth(save.Entries["a"])); } public void TableInsideItself_IsReadAsATableHoldingItself() { SaveNode save = LuaSaveFormat.Parse("local loop = {} loop.self = loop return loop"); CheckTrue(ReferenceEquals(save, save.Entries["self"])); } public void SameLuaTableInTwoPlaces_IsOneNode() { SaveNode save = LuaSaveFormat.Parse("local point = { x = 'a' } return { p = point, q = point }"); CheckTrue(ReferenceEquals(save.Entries["p"], save.Entries["q"])); CheckEqual("a", save.Entries["p"].Entries["x"].Text); } public void TwoLuaTablesWithEqualContents_AreTwoNodes() { SaveNode save = LuaSaveFormat.Parse("return { p = { x = 'a' }, q = { x = 'a' } }"); CheckFalse(ReferenceEquals(save.Entries["p"], save.Entries["q"])); } public void SameLuaListAndMapInTwoPlaces_AreOneNodeEach() { SaveNode save = LuaSaveFormat.Parse( "local list = { 'a' } local map = { [{ x = 'a' }] = 'v' } " + "return { l1 = list, l2 = list, m1 = map, m2 = map }"); CheckTrue(ReferenceEquals(save.Entries["l1"], save.Entries["l2"])); CheckTrue(ReferenceEquals(save.Entries["m1"], save.Entries["m2"])); CheckEqual(SaveNode.Kinds.List, save.Entries["l1"].Kind); CheckEqual(SaveNode.Kinds.Map, save.Entries["m1"].Kind); } public void SharedTableTheWriterMade_IsOneNodeAndWritesBackIdentically() { SaveNode point = PointNode("1", "2"); string text = LuaSaveFormat.Format(SaveOf(Entry("a", point), Entry("b", point))); SaveNode read = LuaSaveFormat.Parse(text); CheckTrue(ReferenceEquals(read.Entries["a"], read.Entries["b"])); CheckEqual("2", read.Entries["a"].Entries["y"].Text); CheckEqual(text, LuaSaveFormat.Format(read)); } public void SharedNodesInsideSharedNodes_WriteBackIdentically() { SaveNode inner = PointNode("1", "2"); SaveNode outer = SaveNode.NewTable(); outer.Entries["inner"] = inner; string text = LuaSaveFormat.Format(SaveOf(Entry("a", outer), Entry("b", outer), Entry("c", inner))); CheckEqual(text, LuaSaveFormat.Format(LuaSaveFormat.Parse(text))); } public void NullInsideASharedTable_IsRead() { SaveNode node = SaveNode.NewTable(); node.Entries["gone"] = SaveNode.Null; SaveNode read = LuaSaveFormat.Parse(LuaSaveFormat.Format(SaveOf(Entry("a", node), Entry("b", node)))); CheckEqual(SaveNode.Kinds.Null, read.Entries["a"].Entries["gone"].Kind); } public void TextMadeByLeafAndUsedTwice_IsOneNode() { SaveNode save = LuaSaveFormat.Parse("local t = leaf('x') return { a = t, b = t }"); CheckEqual("x", save.Entries["a"].Text); CheckTrue(ReferenceEquals(save.Entries["a"], save.Entries["b"])); } public void TwoCallsToLeafWithEqualText_AreTwoNodes() { SaveNode save = LuaSaveFormat.Parse("return { a = leaf('x'), b = leaf('x') }"); CheckFalse(ReferenceEquals(save.Entries["a"], save.Entries["b"])); } public void SharedTextTheWriterMade_IsOneNodeAndWritesBackIdentically() { SaveNode text = SaveNode.OfText("t"); SaveNode list = SaveNode.NewList(); list.Items.Add(text); list.Items.Add(text); string written = FormatHolding(list); SaveNode read = LuaSaveFormat.Parse(written); CheckTrue(ReferenceEquals(read.Entries["k"].Items[0], read.Entries["k"].Items[1])); CheckEqual(written, LuaSaveFormat.Format(read)); } public void LeafGivenSomethingOtherThanText_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { a = leaf(5) }"); } public void TableNamedSharedInTheSave_IsAnOrdinaryEntry() { SaveNode read = LuaSaveFormat.Parse("return { shared = { x = 'a' } }"); CheckEqual("a", read.Entries["shared"].Entries["x"].Text); } public void SaveThatNeverFinishes_IsRejected() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("while true do end", maximumInstructions: 1000000); } public void SaveThatRunsPastTheLimit_IsRejected() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("for i = 1, 1000000 do end return {}", maximumInstructions: 100000); } public void SaveThatRunsWithinTheLimit_IsRead() { SaveNode save = LuaSaveFormat.Parse("local sum = 0 for i = 1, 1000 do sum = sum + 1 end return {}", maximumInstructions: 1000000); CheckEqual(SaveNode.Kinds.Table, save.Kind); } public void SaveWithManyTablesToBuild_IsReadUnderTheDefaultLimit() { SaveNode save = LuaSaveFormat.Parse("local t = {} for i = 1, 100000 do t[i] = { i } end return {}"); CheckEqual(SaveNode.Kinds.Table, save.Kind); } public void SaveThatRunsOutOfMemory_IsRejected() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("local text = string.rep('x', 2000000000) return {}"); } public void NestingFarPastWhatTheCallingThreadsStackAllows_IsRead() { const int depth = 20000; string unused = new string('{', depth) + new string('}', depth); SaveNode save = LuaSaveFormat.Parse("local unused = " + unused + " return {}"); CheckEqual(SaveNode.Kinds.Table, save.Kind); } public void NestingPastTheStack_NamesTheCause() { InvalidDataException thrown = null; try { ParseOnSmallStack("local unused = " + Repeated("{", GiveUpDepth) + Repeated("}", GiveUpDepth) + " return {}"); } catch (InvalidDataException exception) { thrown = exception; } CheckNotNull(thrown); CheckEqual("Malformed save: chunk has too many syntax levels", thrown.Message); } public void ModestNesting_IsReadOnASmallStack() { SaveNode save = ParseOnSmallStack("return { a = " + NestedLua(SaveNode.MaxDepth - 1) + " }"); CheckEqual(SaveNode.Kinds.Table, save.Entries["a"].Kind); } public void NestedTables_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = " + Repeated("{", depth) + Repeated("}", depth) + " return {}") > 0); } public void NestedParentheses_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = " + Repeated("(", depth) + "1" + Repeated(")", depth) + " return {}") > 0); } public void NestedNots_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = " + Repeated("not ", depth) + "1 return {}") > 0); } public void NestedNegations_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = " + Repeated("- ", depth) + "1 return {}") > 0); } public void NestedFunctions_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = " + Repeated("function() return ", depth) + "1" + Repeated(" end", depth) + " return {}") > 0); } public void NestedBlocks_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("do ", depth) + Repeated("end ", depth) + "return {}") > 0); } public void NestedIfs_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("if true then ", depth) + Repeated("end ", depth) + "return {}") > 0); } public void NestedWhiles_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("while false do ", depth) + Repeated("end ", depth) + "return {}") > 0); } public void NestedRepeats_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("repeat ", depth) + Repeated("until true ", depth) + "return {}") > 0); } public void NestedNumericFors_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("for i = 1, 0 do ", depth) + Repeated("end ", depth) + "return {}") > 0); } public void NestedGenericFors_AreReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => Repeated("for key in next, {} do ", depth) + Repeated("end ", depth) + "return {}") > 0); } public void ChainOfAdditions_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = 1" + Repeated(" + 1", depth) + " return {}") > 0); } public void ChainOfConcatenations_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = 'a'" + Repeated(" .. 'a'", depth) + " return {}") > 0); } public void ChainOfPowers_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = 1" + Repeated(" ^ 1", depth) + " return {}") > 0); } public void ChainOfAnds_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local unused = 1" + Repeated(" and 1", depth) + " return {}") > 0); } public void ChainOfIndexes_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local t = {} if false then local unused = t" + Repeated("[1]", depth) + " end return {}") > 0); } public void ChainOfCalls_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local f = print if false then local unused = f" + Repeated("()", depth) + " end return {}") > 0); } public void ChainOfIndexedAssignments_IsReadUntilTheStackRunsLow() { CheckTrue(FirstDepthRejected(depth => "local t = {} if false then t" + Repeated("[1]", depth) + " = 1 end return {}") > 0); } public void WaysOutOfTheSandbox_AreAllAbsent() { foreach (string name in new[] { "os", "io", "load", "loadstring", "loadfile", "dofile", "require" }) { CheckEqual("nil", LuaSaveFormat.Parse("return { kind = type(" + name + ") }").Entries["kind"].Text); } } public void List_IsReadInOrder() { SaveNode list = LuaSaveFormat.Parse("return { k = { 'b', 'a', 'c' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.List, list.Kind); CheckEqual(new[] { "b", "a", "c" }, list.Items.Select(item => item.Text).ToArray()); } public void ListWithNumberedEntriesWrittenOutOfOrder_IsReadByNumber() { SaveNode list = LuaSaveFormat.Parse("return { k = { [2] = 'b', [1] = 'a' } }").Entries["k"]; CheckEqual(new[] { "a", "b" }, list.Items.Select(item => item.Text).ToArray()); } public void NullMarkerInAList_IsNull() { SaveNode list = LuaSaveFormat.Parse("return { k = { 'a', null } }").Entries["k"]; CheckEqual(SaveNode.Kinds.Null, list.Items[1].Kind); } public void ListInsideATableInsideAList_IsRead() { SaveNode list = LuaSaveFormat.Parse("return { k = { { names = { 'x', 'y' } } } }").Entries["k"]; SaveNode names = list.Items[0].RequireEntry("names"); CheckEqual(new[] { "x", "y" }, names.Items.Select(item => item.Text).ToArray()); } public void ListInsideAList_IsRead() { SaveNode list = LuaSaveFormat.Parse("return { k = { { 'a' }, { 'b', 'c' } } }").Entries["k"]; CheckEqual(2, list.Items.Count); CheckEqual(SaveNode.Kinds.List, list.Items[1].Kind); CheckEqual("c", list.Items[1].Items[1].Text); } public void EmptyBraces_AreATableThatRequireItemsTakesAsAnEmptyList() { SaveNode empty = LuaSaveFormat.Parse("return { k = {} }").Entries["k"]; CheckEqual(SaveNode.Kinds.Table, empty.Kind); CheckEqual(0, empty.RequireItems().Count); } public void WhatFormatWrote_IsWrittenAgainIdentically() { SaveNode save = SaveNode.NewTable(); save.Entries["short"] = ListOf("b", null, "a"); save.Entries["long"] = ListOf("a", "b", "c", "d"); save.Entries["wide"] = ListOneLineWide(LuaSaveFormat.LineWidth + 1); SaveNode people = SaveNode.NewList(); SaveNode person = SaveNode.NewTable(); person.Entries["tags"] = ListOf("x", "y"); people.Items.Add(person); people.Items.Add(ListOf("p", "q")); people.Items.Add(SaveNode.OfText("loose")); save.Entries["people"] = people; string written = LuaSaveFormat.Format(save); CheckEqual(written, LuaSaveFormat.Format(LuaSaveFormat.Parse(written))); } public void ListsNestedAsDeepAsAllowed_AreRead() { SaveNode save = LuaSaveFormat.Parse("return { a = " + NestedListLua(SaveNode.MaxDepth - 1) + " }"); CheckEqual(SaveNode.Kinds.List, save.Entries["a"].Kind); } public void ListsNestedFarBeyondTheWriterDepth_AreRead() { SaveNode save = LuaSaveFormat.Parse("return { a = " + NestedListLua(500) + " }"); CheckEqual(500, ListDepth(save.Entries["a"])); } public void ListInsideItself_IsReadAsAListHoldingItself() { SaveNode save = LuaSaveFormat.Parse("local loop = {} loop[1] = loop return { a = loop }"); SaveNode list = save.Entries["a"]; CheckTrue(ReferenceEquals(list, list.Items[0])); } public void GapInTheNumbering_NamesTheTable() { string message = ParseFailure("return { fields = { k = { [1] = 'a', [3] = 'c' } } }"); CheckTrue(message != null && message.Contains("fields.k must be numbered from 1 with no gaps")); } public void NumberingThatStartsAtZero_Throws() { CheckTrue(ParseFailure("return { k = { [0] = 'a', [1] = 'b' } }") != null); } public void NegativeNumber_Throws() { CheckTrue(ParseFailure("return { k = { [-1] = 'a' } }") != null); } public void FractionalNumber_Throws() { CheckTrue(ParseFailure("return { k = { [1] = 'a', [1.5] = 'b' } }") != null); } public void NamedAndNumberedEntriesTogether_NameTheTable() { string message = ParseFailure("return { when = { 'a', ticks = 'b' } }"); CheckTrue(message != null && message.Contains("when mixes named and numbered entries")); } public void SaveThatIsAList_SaysSo() { string message = ParseFailure("return { 'a' }"); CheckTrue(message != null && message.Contains("must be a table, not a list")); } public void ItemOfTheWrongType_NamesItsPosition() { string message = ParseFailure("return { fields = { waypoints = { 'a', 'b', function() end } } }"); CheckTrue(message != null && message.Contains("fields.waypoints[3]")); } public void ItemInsideAListInsideAList_NamesBothPositions() { string message = ParseFailure("return { k = { 'a', { 'b', function() end } } }"); CheckTrue(message != null && message.Contains("k[2][2]")); } public void TableKeys_ReadAsAMap() { SaveNode map = LuaSaveFormat.Parse("return { k = { [{ x = 'a' }] = 'home' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.Map, map.Kind); CheckEqual(1, map.Pairs.Count); CheckEqual("a", map.Pairs[0].Key.RequireEntry("x").Text); CheckEqual("home", map.Pairs[0].Value.Text); } public void ListKey_ReadsAsAList() { SaveNode map = LuaSaveFormat.Parse("return { k = { [{ 'a', 'b' }] = 'v' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.List, map.Pairs[0].Key.Kind); CheckEqual("b", map.Pairs[0].Key.Items[1].Text); } public void EmptyKey_ReadsAsAnEmptyTable() { SaveNode map = LuaSaveFormat.Parse("return { k = { [{}] = 'v' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.Table, map.Pairs[0].Key.Kind); CheckEqual(0, map.Pairs[0].Key.Entries.Count); } public void ValuesOfAMap_CanBeAnyNode() { SaveNode map = LuaSaveFormat.Parse( "return { k = { [{ x = 'a' }] = { 'p', 'q' }, [{ x = 'b' }] = null } }").Entries["k"]; CheckEqual(SaveNode.Kinds.List, map.Pairs[0].Value.Kind); CheckEqual(SaveNode.Kinds.Null, map.Pairs[1].Value.Kind); } public void MapPairs_AreSortedByTheKeysText() { SaveNode map = LuaSaveFormat.Parse( "return { k = { [{ x = 'c' }] = '3', [{ x = 'a' }] = '1', [{ x = 'b' }] = '2' } }").Entries["k"]; CheckEqual(new[] { "1", "2", "3" }, map.Pairs.Select(pair => pair.Value.Text).ToArray()); } public void TextKeys_ReadAsATableThatRequireMapTakesAsTextKeys() { SaveNode table = LuaSaveFormat.Parse("return { k = { b = '2', a = '1' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.Table, table.Kind); CheckEqual(new[] { "a", "b" }, table.RequireMap().Select(pair => pair.Key.Text).ToArray()); } public void KeysWithIdenticalContents_AreBothKept() { SaveNode map = LuaSaveFormat.Parse( "return { k = { [{ x = 'a' }] = '1', [{ x = 'a' }] = '2' } }").Entries["k"]; CheckEqual(2, map.Pairs.Count); } public void WhatFormatWroteForMaps_IsWrittenAgainIdentically() { SaveNode save = SaveNode.NewTable(); save.Entries["byPoint"] = MapOf(Pair(PointNode("3", "4"), ListOf("a", "b", "c", "d")), Pair(PointNode("1", "2"), SaveNode.Null)); save.Entries["byList"] = MapOf(Pair(ListOf("p", "q"), PointNode("5", "6")), Pair(ListOf("p"), SaveNode.OfText("one"))); save.Entries["byText"] = MapOf(Pair(SaveNode.OfText("b"), SaveNode.OfText("1")), Pair(SaveNode.OfText("a"), SaveNode.OfText("2"))); SaveNode list = SaveNode.NewList(); list.Items.Add(MapOf(Pair(PointNode("7", "8"), SaveNode.OfText("in a list")))); save.Entries["inAList"] = list; string written = LuaSaveFormat.Format(save); CheckEqual(written, LuaSaveFormat.Format(LuaSaveFormat.Parse(written))); } public void KeyNestedAsDeepAsAllowed_IsRead() { SaveNode map = LuaSaveFormat.Parse( "return { k = { [" + NestedLua(SaveNode.MaxDepth - 1) + "] = 'v' } }").Entries["k"]; CheckEqual(SaveNode.Kinds.Table, map.Pairs[0].Key.Kind); } public void KeyNestedTooDeeply_Throws() { CheckThrow(typeof(InvalidDataException)); LuaSaveFormat.Parse("return { k = { [" + NestedLua(SaveNode.MaxDepth) + "] = 'v' } }"); } public void TableKeyMixedWithATextKey_NamesTheMap() { string message = ParseFailure("return { fields = { [{}] = 'a', b = 'c' } }"); CheckTrue(message != null && message.Contains("fields mixes table keys with other keys")); } public void TableKeyMixedWithANumber_Throws() { CheckTrue(ParseFailure("return { k = { [{}] = 'a', 'b' } }") != null); } public void NullKey_NamesTheEntry() { string message = ParseFailure("return { k = { [null] = 'a' } }"); CheckTrue(message != null && message.Contains("k[key 1] can't be null")); } public void MalformedKey_NamesTheKeyAndWhatIsWrongInIt() { string message = ParseFailure("return { k = { [{ x = function() end }] = 'a' } }"); CheckTrue(message != null && message.Contains("k[key 1].x")); } public void MalformedValue_NamesTheEntry() { string message = ParseFailure("return { k = { [{ x = 'a' }] = function() end } }"); CheckTrue(message != null && message.Contains("k[value 1]")); } public void KeyHoldingAMapWithTableKeys_Throws() { string message = ParseFailure("return { k = { [{ [{}] = 'v' }] = 'w' } }"); CheckTrue(message != null && message.Contains("a map's key can't hold a map")); } public void SaveThatIsAMap_SaysSo() { string message = ParseFailure("return { [{}] = 'a' }"); CheckTrue(message != null && message.Contains("must be a table, not a map")); } } public class WriteTests : TestFixture { public void WritesUtf8WithoutAByteOrderMark() { SaveNode save = SaveNode.NewTable(); save.Entries["name"] = SaveNode.OfText("café"); var stream = new MemoryStream(); LuaSaveFormat.Write(save, stream); CheckEqual(Encoding.UTF8.GetBytes(LuaSaveFormat.Format(save)), stream.ToArray()); } public void LeavesTheStreamOpen() { var stream = new MemoryStream(); LuaSaveFormat.Write(SaveNode.NewTable(), stream); CheckTrue(stream.CanWrite); } } public class ReadTests : TestFixture { public void ReadsWhatWriteWrote() { var stream = new MemoryStream(); LuaSaveFormat.Write(SampleSave(), stream); stream.Position = 0; CheckEqual("last", LuaSaveFormat.Read(stream).Entries["zeta"].Text); } public void LeavesTheStreamOpen() { var stream = new MemoryStream(Encoding.UTF8.GetBytes("return {}")); LuaSaveFormat.Read(stream); CheckTrue(stream.CanRead); } } } }