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);
}
}
}
}