From bc478b735945f26905103d911152587910d3bd18 Mon Sep 17 00:00:00 2001 From: molsonkiko <46202915+molsonkiko@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:27:53 -0700 Subject: [PATCH] first ini file parser that seems to work fine still need to add more ini parser tests, integrate into main UI should decide whether to implement interpolation too --- CHANGELOG.md | 1 + JsonToolsNppPlugin/JSONTools/IniFileParser.cs | 315 ++++++++++++++---- JsonToolsNppPlugin/JSONTools/JNode.cs | 47 ++- JsonToolsNppPlugin/Properties/AssemblyInfo.cs | 4 +- .../Tests/IniFileParserTests.cs | 63 ++-- JsonToolsNppPlugin/Tests/TestRunner.cs | 15 +- JsonToolsNppPlugin/Utils/Settings.cs | 4 +- most recent errors.txt | 60 ++-- testfiles/small/example_ini.ini | 17 +- testfiles/small/example_ini.json | 15 +- testfiles/small/example_ini_reformatted.ini | 45 +++ 11 files changed, 454 insertions(+), 132 deletions(-) create mode 100644 testfiles/small/example_ini_reformatted.ini diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ab62c..3eba6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +1. Add parser for `.ini` files, allowing them to be reformatted and edited with the tree view. 1. [Python-style spreading of an array](/docs/RemesPath.md#spreading-function-args-to-fill-multiple-arguments-added-in-v58) with `*` to fill multiple arguments of a function. 2. New RemesPath function(s): [`at` function](/docs/RemesPath.md#non-vectorized-functions) for indexing into array or object at an index or key determined at runtime (since indexing with square braces does not support keys/indices that are functions of input). 3. Made it possible to customize newline for CSV files generated by the [JSON-to-CSV form](/docs/README.md#json-to-csv). diff --git a/JsonToolsNppPlugin/JSONTools/IniFileParser.cs b/JsonToolsNppPlugin/JSONTools/IniFileParser.cs index 83736d8..9801f66 100644 --- a/JsonToolsNppPlugin/JSONTools/IniFileParser.cs +++ b/JsonToolsNppPlugin/JSONTools/IniFileParser.cs @@ -1,32 +1,49 @@ using System.Collections.Generic; using System; - +using System.Text.RegularExpressions; +using System.Security.Principal; namespace JSON_Tools.JSON_Tools { - public class IniParserException : Exception + public class IniParserException : FormatException { - public IniParserException(string message) : base(message) { } + public new string Message { get; set; } + public char CurChar { get; set; } + public int Position { get; set; } + + public IniParserException(string Message, char c, int pos) + { + this.Message = Message; + this.CurChar = c; + this.Position = pos; + } + + public override string ToString() + { + return $"{Message} at position {Position} (char {JsonLint.CharDisplay(CurChar)})"; + } } public class IniFileParser { - /// - /// if true, ${SECTION:KEY} is replaced with the value associated with key KEY in section SECTION

- /// ${KEY} is replaced with the value associated with key KEY if KEY is in the same section - ///
- public bool UseInterpolation; + ///// + ///// if true, ${SECTION:KEY} is replaced with the value associated with key KEY in section SECTION

+ ///// ${KEY} is replaced with the value associated with key KEY if KEY is in the same section + /////
+ //public bool UseInterpolation; + public int utf8ExtraBytes { get; private set; } public int ii { get; private set; } + public int lineNum { get; private set; } public int utf8Pos { get { return ii + utf8ExtraBytes; } } public List comments; - public IniFileParser(bool useInterpolation) + public IniFileParser(/*bool useInterpolation*/) { ii = 0; utf8ExtraBytes = 0; comments = new List(); - UseInterpolation = useInterpolation; + //UseInterpolation = useInterpolation; } public void Reset() @@ -36,71 +53,220 @@ public void Reset() comments.Clear(); } - //public static readonly Regex INI_TOKENIZER = new Regex( - // @"^\s*\[([^\r\n]+)\]\s*$|" + // section name - // @"^\s*([^\r\n:=]+)\s*[:=](.+)$|" + // key and value within section - // @"^\s*[;#](.+)$|" + // comment - // @"^\s*(?![;#])(\S+)$" // multi-line continuation of value - //); - - public JObject Parse(string text) + public JObject Parse(string inp) { Reset(); var doc = new Dictionary(); - return new JObject(); - while (ii < text.Length) + while (ii < inp.Length) { - + ConsumeInsignificantChars(inp); + char c = inp[ii++]; + if (c == '[') + { + // section header + int startOfHeader = ii; + int headerStartUtf8 = ii - 1 + utf8ExtraBytes; + int endOfHeader = -1; + // find end of header, then keep going to start of next line, + // throwing an exception if newline comes before end of header, + // or non-whitespace comes between end of header and newline + while (ii < inp.Length) + { + c = inp[ii]; + switch (c) + { + case ']': + endOfHeader = ii; + break; + case ' ': + case '\t': + case '\r': + break; + case '\u2028': + case '\u2029': + case '\ufeff': + case '\xa0': + case '\u1680': + case '\u2000': + case '\u2001': + case '\u2002': + case '\u2003': + case '\u2004': + case '\u2005': + case '\u2006': + case '\u2007': + case '\u2008': + case '\u2009': + case '\u200A': + case '\u202F': + case '\u205F': + case '\u3000': + utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); + break; + case '\n': + ii++; + lineNum++; + if (endOfHeader < 0) + throw new IniParserException("Opening '[' and closing ']' of section header were not on the same line", c, headerStartUtf8 - 1); + else + goto headerFinished; + default: + if (endOfHeader > 0) + throw new IniParserException("Non-whitespace between end of section header and end of header line", '[', headerStartUtf8 - 1); + utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); + break; + } + ii++; + } + headerFinished: + string header = JNode.StrToString(inp.Substring(startOfHeader, endOfHeader - startOfHeader), false); + if (doc.ContainsKey(header)) + { + throw new IniParserException($"Document has duplicate section header [{header}]", c, headerStartUtf8 - 1); + } + JObject section = ParseSection(inp, header); + section.position = headerStartUtf8; + doc[header] = section; + } } + return new JObject(0, doc); } - public JObject ParseSection(string text, int sectionIndent) + public JObject ParseSection(string inp, string header) { var section = new Dictionary(); - return new JObject(); - while (ii < text.Length) + while (ii < inp.Length) { - int curIndent = ConsumeInsignificantChars(text); - char c = text[ii]; + int indent = ConsumeInsignificantChars(inp); + int keyStartUtf8 = utf8Pos; + if (ii >= inp.Length) + break; + char c = inp[ii]; + if (c == '[') + // start of the next section + break; + KeyValuePair kv = ParseKeyValuePair(inp, indent); + if (section.ContainsKey(kv.Key)) + { + throw new IniParserException($"Section [{header}] has duplicate key \"{kv.Key}\"", c, keyStartUtf8); + } + section[kv.Key] = kv.Value; } - int startOfSectionName = ii; + return new JObject(0, section); } - public static JObject Interpolate(JObject iniJson) + public KeyValuePair ParseKeyValuePair(string inp, int keyIndent) { - return iniJson; + if (ii >= inp.Length) + throw new IniParserException("EOF when expecting key-value pair", inp[inp.Length - 1], inp.Length - 1); + int startOfKey = ii; + char startC = inp[ii]; + int keyStartUtf8 = utf8Pos; + int endOfKey = ii; + bool foundKeyValueSep = false; + while (ii < inp.Length) + { + char c = inp[ii]; + switch (c) + { + case '=': + case ':': + foundKeyValueSep = true; + break; + case ' ': + case '\t': + case '\r': + break; + case '\u2028': + case '\u2029': + case '\ufeff': + case '\xa0': + case '\u1680': + case '\u2000': + case '\u2001': + case '\u2002': + case '\u2003': + case '\u2004': + case '\u2005': + case '\u2006': + case '\u2007': + case '\u2008': + case '\u2009': + case '\u200A': + case '\u202F': + case '\u205F': + case '\u3000': + utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); + break; + case '\n': + if (!foundKeyValueSep) + throw new IniParserException("No '=' or ':' on same line as key", startC, keyStartUtf8); + // key with empty value + goto exitKeyLoop; + default: + utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); + if (foundKeyValueSep) + { + // first non-whitespace char after ':' or '=' + goto exitKeyLoop; + } + // last non-whitespace seen so far before ':' or '=' + endOfKey = ii; + break; + } + ii++; + } + exitKeyLoop: + string key = JNode.StrToString(inp.Substring(startOfKey, endOfKey + 1 - startOfKey), false); + int startOfValue = ii; + int valueStartUtf8 = utf8Pos; + int endOfValue = ii; + while (ii < inp.Length) + { + int lineStart = ii; + ConsumeLine(inp); + endOfValue = JsonParser.EndOfPreviousLine(inp, ii, lineStart); + // tentatively ending value at end of last line + // we'll check to see if the current line has non-whitespace, non-comment at a greater indent than the start of the key + // if it does, the value will continue to the end of this line. + int indent = ConsumeToIndent(inp); + if (ii < inp.Length) + { + if (indent > keyIndent) + { + char c = inp[ii]; + if (c == ';' || c == '#') + // start of comment, so value is over + break; + } + else + break; // value ends with line that has indent <= keyIndent. + } + } + string valueStr = inp.Substring(startOfValue, endOfValue - startOfValue); + JNode valueNode = new JNode(valueStr, valueStartUtf8); + return new KeyValuePair(key, valueNode); } + //public static JObject Interpolate(JObject iniJson) + //{ + // return iniJson; + //} + /// - /// Consume comments and whitespace until the next character that is not - /// '#', ';', or whitespace. + /// Consume comments and whitespace until EOF or the next character that is not + /// whitespace or the beginning of a comment.

+ /// Return the indent of the current position, as described by ConsumeToIndent below. ///
/// the number of characters since the last newline private int ConsumeInsignificantChars(string inp) { - bool isFirstNonWhiteSpaceCharOfLine = false; - int charsSinceLastNewline = -1; + int indent = 0; while (ii < inp.Length) { char c = inp[ii]; - charsSinceLastNewline++; switch (c) { - case '\n': - ii++; - charsSinceLastNewline = 0; - isFirstNonWhiteSpaceCharOfLine = true; - break; - case ';': - case '#': - if (!isFirstNonWhiteSpaceCharOfLine) - throw new IniParserException("JsonTools cannot parse ini file comments where the comment start character ('#' or ';') is not the first non-whitespace character of the line"); - int commentStartUtf8 = utf8Pos; - int commentContentStartII = ii + 1; - ConsumeLine(inp); - int commentContentEndII = JsonParser.EndOfPreviousLine(inp, ii, commentContentStartII); - comments.Add(new Comment(inp.Substring(commentContentStartII, commentContentEndII - commentContentStartII), false, commentStartUtf8)); - break; case ' ': case '\t': case '\r': @@ -123,16 +289,32 @@ private int ConsumeInsignificantChars(string inp) case '\u202F': case '\u205F': case '\u3000': + indent++; + ii++; utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); + break; + case '\n': + lineNum++; ii++; + indent = 0; + break; + case ';': + case '#': + int commentStartUtf8 = utf8Pos; + int commentContentStartII = ii + 1; + ConsumeLine(inp); + int commentContentEndII = JsonParser.EndOfPreviousLine(inp, ii, commentContentStartII); + comments.Add(new Comment(inp.Substring(commentContentStartII, commentContentEndII - commentContentStartII), false, commentStartUtf8)); + indent = 0; break; - default: return charsSinceLastNewline; + default: return indent; } } - return charsSinceLastNewline; + return 0; } /// + /// Move ii to the first character of the next line (after the next newline), or to EOF

/// note that both this and ConsumeLineAndGetLastNonWhitespace treat '\n' as the newline character, /// because this works for both Windows CRLF and Unix LF. However, Macintosh CR is not supported. ///
@@ -143,15 +325,27 @@ private void ConsumeLine(string inp) { char c = inp[ii++]; if (c == '\n') + { + lineNum++; return; + } utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); } } - private int ConsumeLineAndGetLastNonWhitespace(string inp) + /// + /// consume characters from this line until '\n', the next non-whitespace, or EOF.

+ /// Return 0 if we stopped on a newline or EOF.

+ /// If we stopped on non-whitespace, return the number of characters between our starting ii (the beginning of the line) + /// and the current ii.

+ /// For example, if we start in position 1 of the string "\n \tf", we will get an indent of 2 because there are + /// two whitespace chars (' ' and '\t' count equally) between our starting position and the "f".

+ /// But if we start at position 1 of ' \n', we will return 0 because the line had no non-whitespace. + ///
+ /// + private int ConsumeToIndent(string inp) { - int lastNonWhitespace = ii; - return 0; + int indent = 0; while (ii < inp.Length) { char c = inp[ii]; @@ -160,6 +354,7 @@ private int ConsumeLineAndGetLastNonWhitespace(string inp) case ' ': case '\t': case '\r': + break; case '\u2028': case '\u2029': case '\ufeff': @@ -182,14 +377,20 @@ private int ConsumeLineAndGetLastNonWhitespace(string inp) utf8ExtraBytes += JsonParser.ExtraUTF8Bytes(c); break; case '\n': + // consumed entire line without seeing non-whitespace + // we treat a line with no non-whitespace characters as having an indent of 0 + // which terminates multi-line strings. ii++; - return lastNonWhitespace; + lineNum++; + return 0; default: - lastNonWhitespace = ii; - break; + // non-whitespace, return # chars since start of line + return indent; } ii++; + indent++; } + return 0; } } } diff --git a/JsonToolsNppPlugin/JSONTools/JNode.cs b/JsonToolsNppPlugin/JSONTools/JNode.cs index d9d0ab3..cc2c2d8 100644 --- a/JsonToolsNppPlugin/JSONTools/JNode.cs +++ b/JsonToolsNppPlugin/JSONTools/JNode.cs @@ -343,6 +343,15 @@ public static string StrToString(string s, bool quoted) return sb.ToString(); } + /// + /// mostly useful for quickly converting a JObject key (which must be escaped during parsing) into the raw string that it represents

+ /// Choose strAlreadyQuoted = true only if str is already wrapped in double quotes. + ///
+ public static string UnescapedJsonString(string str, bool strAlreadyQuoted) + { + return (string)new JsonParser().ParseString(strAlreadyQuoted ? str : $"\"{str}\"").value; + } + /// /// Compactly prints the JSON.

/// If sort_keys is true, the keys of objects are printed in alphabetical order.

@@ -1207,9 +1216,45 @@ public override (int comment_idx, int extra_utf8_bytes) PrettyPrintWithCommentsH return (comment_idx, extra_utf8_bytes); } + /// + /// dump this JObject as an ini file + /// + /// + /// + /// public string ToIniFile(List comments) { - return ""; + var sb = new StringBuilder(); + int positionInComments = 0; + int utf8ExtraBytes = 0; + foreach (KeyValuePair kv in children) + { + string header = kv.Key; + if (!(kv.Value is JObject section)) + { + throw new InvalidOperationException("Only objects where all children are objects with only string values can be converted to ini files"); + } + // section is treated as beginning just before the open squarebrace of the header + (positionInComments, utf8ExtraBytes) = Comment.AppendAllCommentsBeforePosition(sb, comments, positionInComments, utf8ExtraBytes, section.position, "", DocumentType.INI); + string unescapedHeader = UnescapedJsonString(header, false); + sb.Append($"[{unescapedHeader}]\r\n"); + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(unescapedHeader, 0, unescapedHeader.Length); + foreach (KeyValuePair sectKv in section.children) + { + string key = sectKv.Key; + JNode value = sectKv.Value; + if (!(value.value is string valueStr)) + { + throw new InvalidOperationException("Only objects where all children are objects with only string values can be converted to ini files"); + } + (positionInComments, utf8ExtraBytes) = Comment.AppendAllCommentsBeforePosition(sb, comments, positionInComments, utf8ExtraBytes, value.position, "", DocumentType.INI); + string unescapedKey = UnescapedJsonString(key, false); + sb.Append($"{unescapedKey}={valueStr}\r\n"); + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(unescapedKey, 0, unescapedKey.Length); + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(valueStr, 0, valueStr.Length); + } + } + return sb.ToString(); } /// diff --git a/JsonToolsNppPlugin/Properties/AssemblyInfo.cs b/JsonToolsNppPlugin/Properties/AssemblyInfo.cs index a0333ea..6a51c76 100644 --- a/JsonToolsNppPlugin/Properties/AssemblyInfo.cs +++ b/JsonToolsNppPlugin/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // Build Number // Revision // -[assembly: AssemblyVersion("5.7.0.6")] -[assembly: AssemblyFileVersion("5.7.0.6")] +[assembly: AssemblyVersion("5.7.0.7")] +[assembly: AssemblyFileVersion("5.7.0.7")] diff --git a/JsonToolsNppPlugin/Tests/IniFileParserTests.cs b/JsonToolsNppPlugin/Tests/IniFileParserTests.cs index ffd9f03..72886f5 100644 --- a/JsonToolsNppPlugin/Tests/IniFileParserTests.cs +++ b/JsonToolsNppPlugin/Tests/IniFileParserTests.cs @@ -12,14 +12,17 @@ public static bool Test() int ii = 0; int testsFailed = 0; var exampleIniFname = @"plugins\JsonTools\testfiles\small\example_ini.ini"; + var exampleIniReformattedFname = @"plugins\JsonTools\testfiles\small\example_ini_reformatted.ini"; var exampleJsonFname = @"plugins\JsonTools\testfiles\small\example_ini.json"; string exampleJsonText = null; string exampleIniText = null; + string exampleIniReformattedText = null; try { exampleIniText = File.ReadAllText(exampleIniFname); exampleJsonText = File.ReadAllText(exampleJsonFname); + exampleIniReformattedText = File.ReadAllText(exampleIniReformattedFname); } catch { @@ -27,74 +30,89 @@ public static bool Test() Npp.AddLine("Either testfiles/small/example_ini.ini or testfiles/small/example_ini.json was not found in the plugins/JsonTools directory"); } - var testcases = new (string iniText, string correctJsonStrWithoutInterpolation, bool interpolation)[] + var testcases = new (string iniText, string correctJsonStrWithoutInterpolation, /*bool interpolation,*/ string correctReformattedIniText)[] { - (exampleIniText, exampleJsonText, false), - (exampleIniText, exampleJsonText, true), + (exampleIniText, exampleJsonText, /*false,*/ exampleIniReformattedText), + //(exampleIniText, exampleJsonText, true, exampleIniReformattedText), }; var jsonParser = new JsonParser(LoggerLevel.JSON5, false, true, true, true); - - foreach ((string iniText, string correctJsonStrWithoutInterpolation, bool interpolation) in testcases) + int testsPerLoop = 3; + foreach ((string iniText, string correctJsonStrWithoutInterpolation, /*bool interpolation,*/ string correctReformattedIniText) in testcases) { JObject correctJson; - JObject correctJsonWithInterpolation; + //JObject correctJsonWithInterpolation; string correctJsonStr; - ii += 3; - Npp.AddLine($"Parsing ini file (shown here as JSON string)\r\n{JNode.StrToString(iniText, true)}"); + ii += testsPerLoop; + bool hasShownIniFileDescription = false; + void showFileDescription() + { + if (!hasShownIniFileDescription) + { + string iniFileDescription = $"Parsing ini file (shown here as JSON string)\r\n{JNode.StrToString(iniText, true)}"; + hasShownIniFileDescription = true; + Npp.AddLine(iniFileDescription); + } + }; try { correctJson = (JObject)jsonParser.Parse(correctJsonStrWithoutInterpolation); - correctJsonWithInterpolation = IniFileParser.Interpolate(correctJson); - correctJsonStr = interpolation ? correctJsonStrWithoutInterpolation : correctJsonWithInterpolation.PrettyPrint(); - if (interpolation) - correctJson = correctJsonWithInterpolation; + //correctJsonWithInterpolation = IniFileParser.Interpolate(correctJson); + correctJsonStr = correctJson.PrettyPrintWithComments(jsonParser.comments, sort_keys:false); } catch (Exception ex) { + showFileDescription(); Npp.AddLine($"While trying to parse JSON\r\n{correctJsonStrWithoutInterpolation}\r\nGOT EXCEPTION\r\n{ex}"); - testsFailed += 3; + testsFailed += testsPerLoop; continue; } JObject gotJson; - IniFileParser iniParser = new IniFileParser(interpolation); + IniFileParser iniParser = new IniFileParser(); try { gotJson = iniParser.Parse(iniText); } catch (Exception ex) { + showFileDescription(); Npp.AddLine($"While trying to parse ini file\r\nGOT EXCEPTION\r\n{ex}"); - testsFailed += 3; + testsFailed += testsPerLoop; continue; } string gotJsonStr = gotJson.ToString(); if (gotJson.TryEquals(correctJson, out _)) { // test if comments are parsed correctly + string gotJsonWithComments = ""; try { - string gotJsonWithComments = gotJson.ToStringWithComments(iniParser.comments); + gotJsonWithComments = gotJson.PrettyPrintWithComments(iniParser.comments, sort_keys:false); if (gotJsonWithComments != correctJsonStr) { testsFailed++; - Npp.AddLine($"EXPECTED JSON WITH COMMENTS\r\n{correctJsonStr}\r\nGOT JSON WITH COMMENTS\r\n{gotJsonStr}"); + showFileDescription(); + Npp.AddLine($"EXPECTED JSON WITH COMMENTS\r\n{correctJsonStr}\r\nGOT JSON WITH COMMENTS\r\n{gotJsonWithComments}"); } } catch (Exception ex) { - testsFailed++; + testsFailed += 2; + showFileDescription(); Npp.AddLine($"EXPECTED JSON WITH COMMENTS\r\n{correctJsonStr}\r\nGOT EXCEPTION\r\n{ex}"); + continue; } // test if dumping the JSON back to an ini file and re-parsing the dumped ini will return the same JSON string dumpedIniText = ""; try { - dumpedIniText = gotJson.ToIniFile(iniParser.comments); + JObject reParsedJson = (JObject)jsonParser.Parse(gotJsonWithComments); + dumpedIniText = reParsedJson.ToIniFile(jsonParser.comments); } catch (Exception ex) { testsFailed++; - Npp.AddLine($"When ini-dumping JSON\r\n{gotJsonStr}\r\nEXPECTED INI FILE\r\n{correctJsonStr}\r\nGOT EXCEPTION {ex}"); + showFileDescription(); + Npp.AddLine($"When ini-dumping JSON\r\n{gotJsonStr}\r\nEXPECTED INI FILE\r\n{correctReformattedIniText}\r\nGOT EXCEPTION {ex}"); continue; } JObject jsonFromDumpedIniText; @@ -104,19 +122,22 @@ public static bool Test() } catch (Exception ex) { + showFileDescription(); Npp.AddLine($"EXPECTED JSON FROM DUMPED INI FILE\r\n{correctJsonStr}\r\nGOT EXCEPTION\r\n{ex}"); testsFailed++; continue; } if (!jsonFromDumpedIniText.TryEquals(correctJson, out _)) { + showFileDescription(); testsFailed++; Npp.AddLine($"EXPECTED JSON FROM DUMPED INI FILE\r\n{correctJson.ToString()}\r\nGOT JSON\r\n{gotJson.ToString()}"); } } else { - testsFailed += 3; + testsFailed += testsPerLoop; + showFileDescription(); Npp.AddLine($"EXPECTED JSON\r\n{correctJson.ToString()}\r\nGOT JSON\r\n{gotJson.ToString()}"); } } diff --git a/JsonToolsNppPlugin/Tests/TestRunner.cs b/JsonToolsNppPlugin/Tests/TestRunner.cs index 6e44792..de934f0 100644 --- a/JsonToolsNppPlugin/Tests/TestRunner.cs +++ b/JsonToolsNppPlugin/Tests/TestRunner.cs @@ -13,6 +13,8 @@ namespace JSON_Tools.Tests { public class TestRunner { + private const string fuzzTestName = "RemesPath produces sane outputs on randomly generated queries"; + public static async Task RunAll() { Npp.notepad.FileNew(); @@ -39,7 +41,7 @@ public static async Task RunAll() (RemesParserTester.Test, "RemesPath parser and compiler", false, false), (RemesPathThrowsWhenAppropriateTester.Test, "RemesPath throws errors on bad inputs", false, false), (RemesPathAssignmentTester.Test, "RemesPath assignment operations", false, false), - (() => RemesPathFuzzTester.Test(10000, 20), "RemesPath produces sane outputs on randomly generated queries", false, false), + (() => RemesPathFuzzTester.Test(10000, 20), fuzzTestName, false, false), (RemesPathComplexQueryTester.Test, "multi-statement queries in RemesPath", false, false), (JsonSchemaMakerTester.Test, "JsonSchema generator", false, false), @@ -111,6 +113,11 @@ public static async Task RunAll() } skipped.Add(name); } + else if (Main.settings.skip_api_request_and_fuzz_tests && name == fuzzTestName) + { + Npp.AddLine("\r\nskipped RemesPath fuzz tests because settings.skip_api_request_and_fuzz_tests was set to true"); + skipped.Add(name); + } else { Npp.AddLine($@"========================= @@ -129,9 +136,9 @@ public static async Task RunAll() Testing JSON grepper's API request tool ========================= "); - if (Main.settings.skip_api_request_tests) + if (Main.settings.skip_api_request_and_fuzz_tests) { - Npp.AddLine("skipped tests because settings.skip_api_request_tests was set to true"); + Npp.AddLine("skipped tests because settings.skip_api_request_and_fuzz_tests was set to true"); skipped.Add("JSON grepper's API request tool"); } else @@ -141,6 +148,8 @@ Testing JSON grepper's API request tool } } + if (skipped.Count > 0) + Npp.editor.InsertText(header.Length + 2, "Tests skipped: " + string.Join(", ", skipped) + "\r\n"); Npp.editor.InsertText(header.Length + 2, "Tests failed: " + string.Join(", ", failures) + "\r\n"); } } diff --git a/JsonToolsNppPlugin/Utils/Settings.cs b/JsonToolsNppPlugin/Utils/Settings.cs index 5e9fa92..1d913c5 100644 --- a/JsonToolsNppPlugin/Utils/Settings.cs +++ b/JsonToolsNppPlugin/Utils/Settings.cs @@ -135,9 +135,9 @@ public class Settings : SettingsBase Category("Miscellaneous"), DefaultValue("\"[{")] public string try_parse_start_chars { get; set; } - [Description("When running tests, skip the tests that send requests to APIs"), + [Description("When running tests, skip the tests that send requests to APIs and the RemesPath fuzz tests"), Category("Miscellaneous"), DefaultValue(true)] - public bool skip_api_request_tests { get; set; } + public bool skip_api_request_and_fuzz_tests { get; set; } [Description("Which type of newline to use for generated CSV files."), Category("Miscellaneous"), DefaultValue(EndOfLine.LF)] diff --git a/most recent errors.txt b/most recent errors.txt index a9c999e..0129d65 100644 --- a/most recent errors.txt +++ b/most recent errors.txt @@ -1,6 +1,6 @@ -Test results for JsonTools v5.7.0.6 on Notepad++ 8.5.7 64bit +Test results for JsonTools v5.7.0.7 on Notepad++ 8.5.7 64bit NOTE: Ctrl-F (regular expressions *on*) for "Failed [1-9]\d*" to find all failed tests -Tests failed: YAML dumper, INI file parser +Tests failed: YAML dumper ========================= Testing JNode Copy method ========================= @@ -182,59 +182,47 @@ Passed 24 tests. Testing INI file parser ========================= -Parsing ini file (shown here as JSON string) -"# cömment before foo section\r\n[föo]\r\nbär : 草 \r\nbaz : 34\r\n\r\n;comment after foo.baz\r\n\r\nquz = a multiline \r\n response\r\n to quz\r\n#comment ending quz\r\n indented header = in unindented section\r\n \r\n ; but we need a comment at end of indented section\r\n [indented section]\r\n subsection 1 = \"5\"\r\n   \r\n subsection 2 = some more text\r\n  even on another line\r\n   \r\n ; indented comment\r\n   whoa : even more indentation\t\r\n \r\n [triple indentation]\r\n \r\n subsection:1\r\n[ dedented section ]\r\nempty\tvalue\theader : \r\n# the previous section name had an empty string as value\r\nempty =\r\n\r\n[empty section ]\r\n[interpolation example]\r\ndollar sign = use $$ instead\r\nfoo header : tab\tsep\ttext\r\n\r\nbar = ${foo header}\r\n; above line should be same as foo header\r\n [ another empty section]\r\n # but with a comment inside\r\n[interpolation from other sections]\r\nbaz = ${foo reference} ${föo:baz}\r\n\r\n# to test if references to interpolated values work correctly\r\nfoo reference = ${interpolation example:foo header}\r\n[colons_and_equals_signs]\r\nequals=inside value=fine\r\ncolon=inside value : also fine\r\n :even on separate line (if indented)" -EXPECTED JSON -{" another empty section": {}, " dedented section ": {"empty": "", "empty\tvalue\theader": ""}, "colons_and_equals_signs": {"colon": "inside value : also fine\r\n :even on separate line (if indented)", "equals": "inside value=fine"}, "empty section ": {}, "föo": {"bär": "草", "baz": "34", "indented header": "in unindented section", "quz": "a multiline\r\n response to\r\n quz"}, "indented section": {"subsection 1": "\"5\"", "subsection 2": "some more text\r\n  even on another line", "whoa": "even more indentation\t"}, "interpolation example": {"bar": "${foo header}", "dollar sign": "use $$ instead", "foo header": "tab\tsep\ttext"}, "interpolation from other sections": {"baz": "${foo reference} ${föo:baz}", "foo reference": "${interpolation example:foo header}"}, "triple indentation": {"subsection": "1"}} -GOT JSON -{} -Parsing ini file (shown here as JSON string) -"# cömment before foo section\r\n[föo]\r\nbär : 草 \r\nbaz : 34\r\n\r\n;comment after foo.baz\r\n\r\nquz = a multiline \r\n response\r\n to quz\r\n#comment ending quz\r\n indented header = in unindented section\r\n \r\n ; but we need a comment at end of indented section\r\n [indented section]\r\n subsection 1 = \"5\"\r\n   \r\n subsection 2 = some more text\r\n  even on another line\r\n   \r\n ; indented comment\r\n   whoa : even more indentation\t\r\n \r\n [triple indentation]\r\n \r\n subsection:1\r\n[ dedented section ]\r\nempty\tvalue\theader : \r\n# the previous section name had an empty string as value\r\nempty =\r\n\r\n[empty section ]\r\n[interpolation example]\r\ndollar sign = use $$ instead\r\nfoo header : tab\tsep\ttext\r\n\r\nbar = ${foo header}\r\n; above line should be same as foo header\r\n [ another empty section]\r\n # but with a comment inside\r\n[interpolation from other sections]\r\nbaz = ${foo reference} ${föo:baz}\r\n\r\n# to test if references to interpolated values work correctly\r\nfoo reference = ${interpolation example:foo header}\r\n[colons_and_equals_signs]\r\nequals=inside value=fine\r\ncolon=inside value : also fine\r\n :even on separate line (if indented)" -EXPECTED JSON -{" another empty section": {}, " dedented section ": {"empty": "", "empty\tvalue\theader": ""}, "colons_and_equals_signs": {"colon": "inside value : also fine\r\n :even on separate line (if indented)", "equals": "inside value=fine"}, "empty section ": {}, "föo": {"bär": "草", "baz": "34", "indented header": "in unindented section", "quz": "a multiline\r\n response to\r\n quz"}, "indented section": {"subsection 1": "\"5\"", "subsection 2": "some more text\r\n  even on another line", "whoa": "even more indentation\t"}, "interpolation example": {"bar": "${foo header}", "dollar sign": "use $$ instead", "foo header": "tab\tsep\ttext"}, "interpolation from other sections": {"baz": "${foo reference} ${föo:baz}", "foo reference": "${interpolation example:foo header}"}, "triple indentation": {"subsection": "1"}} -GOT JSON -{} -Failed 6 tests. -Passed 0 tests. +Failed 0 tests. +Passed 3 tests. ========================= Testing UI tests ========================= Failed 0 tests -Passed 199 tests +Passed 209 tests ========================= Testing JsonParser performance ========================= Preview of json: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... -To convert JSON string of size 89556 into JNode took 2.644 +/- 1.272 ms over 32 trials -Load times (ms): 1, 3, 2, 1, 1, 5, 4, 2, 2, 4, 1, 1, 2, 3, 1, 1, 4, 1, 1, 1, 2, 1, 1, 4, 1, 1, 2, 4, 2, 2, 5, 1 +To convert JSON string of size 89556 into JNode took 2.786 +/- 1.154 ms over 32 trials +Load times (ms): 4, 2, 3, 4, 4, 1, 2, 2, 3, 1, 1, 5, 1, 1, 1, 2, 1, 1, 3, 4, 1, 1, 2, 3, 2, 3, 3, 1, 2, 3, 2, 3 ========================= Performance tests for RemesPath (float arithmetic) ========================= -Compiling query "@[@[:].a * @[:].t < @[:].e]" into took 1.295 +/- 9.777 microseconds over 64 trials -To run pre-compiled query "@[@[:].a * @[:].t < @[:].e]" on JNode from JSON of size 89556 into took 0.025 +/- 0.008 ms over 64 trials -Query times (ms): 0.089, 0.036, 0.024, 0.023, 0.024, 0.023, 0.023, 0.023, 0.023, 0.022, 0.023, 0.028, 0.024, 0.023, 0.023, 0.022, 0.023, 0.022, 0.022, 0.023, 0.022, 0.026, 0.023, 0.024, 0.023, 0.022, 0.023, 0.023, 0.023, 0.022, 0.022, 0.026, 0.023, 0.023, 0.023, 0.023, 0.023, 0.023, 0.023, 0.023, 0.023, 0.026, 0.023, 0.023, 0.023, 0.023, 0.024, 0.022, 0.022, 0.023, 0.023, 0.026, 0.023, 0.024, 0.028, 0.023, 0.023, 0.023, 0.022, 0.022, 0.023, 0.026, 0.023, 0.023 +Compiling query "@[@[:].a * @[:].t < @[:].e]" into took 1.409 +/- 10.695 microseconds over 64 trials +To run pre-compiled query "@[@[:].a * @[:].t < @[:].e]" on JNode from JSON of size 89556 into took 0.075 +/- 0.366 ms over 64 trials +Query times (ms): 0.068, 2.95, 0.025, 0.024, 0.022, 0.022, 0.022, 0.023, 0.023, 0.022, 0.023, 0.025, 0.022, 0.022, 0.022, 0.023, 0.022, 0.022, 0.022, 0.022, 0.022, 0.023, 0.022, 0.022, 0.022, 0.022, 0.022, 0.021, 0.022, 0.022, 0.022, 0.023, 0.021, 0.022, 0.022, 0.022, 0.022, 0.022, 0.022, 0.022, 0.021, 0.022, 0.022, 0.021, 0.022, 0.022, 0.022, 0.022, 0.022, 0.022, 0.022, 0.023, 0.023, 0.029, 0.022, 0.023, 0.023, 0.023, 0.023, 0.022, 0.023, 0.417, 0.023, 0.022 Preview of result: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... ========================= Performance tests for RemesPath (string operations) ========================= -Compiling query "@[@[:].z =~ `(?i)[a-z]{5}`]" into took 0.939 +/- 6.862 microseconds over 64 trials -To run pre-compiled query "@[@[:].z =~ `(?i)[a-z]{5}`]" on JNode from JSON of size 89556 into took 0.054 +/- 0.01 ms over 64 trials -Query times (ms): 0.128, 0.055, 0.054, 0.07, 0.077, 0.051, 0.051, 0.051, 0.052, 0.051, 0.051, 0.053, 0.054, 0.052, 0.052, 0.051, 0.051, 0.05, 0.051, 0.051, 0.051, 0.054, 0.053, 0.053, 0.052, 0.051, 0.051, 0.051, 0.051, 0.05, 0.052, 0.051, 0.052, 0.05, 0.051, 0.051, 0.05, 0.05, 0.051, 0.051, 0.05, 0.051, 0.051, 0.072, 0.05, 0.052, 0.051, 0.051, 0.051, 0.05, 0.051, 0.052, 0.052, 0.051, 0.052, 0.051, 0.054, 0.052, 0.053, 0.051, 0.051, 0.052, 0.06, 0.054 +Compiling query "@[@[:].z =~ `(?i)[a-z]{5}`]" into took 1.136 +/- 8.475 microseconds over 64 trials +To run pre-compiled query "@[@[:].z =~ `(?i)[a-z]{5}`]" on JNode from JSON of size 89556 into took 0.069 +/- 0.045 ms over 64 trials +Query times (ms): 0.112, 0.127, 0.065, 0.052, 0.051, 0.052, 0.051, 0.052, 0.052, 0.051, 0.052, 0.051, 0.052, 0.052, 0.052, 0.051, 0.052, 0.051, 0.051, 0.052, 0.051, 0.052, 0.051, 0.052, 0.051, 0.051, 0.05, 0.051, 0.051, 0.051, 0.34, 0.054, 0.052, 0.052, 0.052, 0.119, 0.122, 0.103, 0.103, 0.152, 0.123, 0.128, 0.072, 0.063, 0.053, 0.186, 0.053, 0.052, 0.051, 0.052, 0.051, 0.052, 0.05, 0.052, 0.052, 0.05, 0.051, 0.051, 0.052, 0.051, 0.051, 0.051, 0.052, 0.051 Preview of result: [{"A": "\n]o1VQ5t6g", "a": 4710024278, "b": 3268860721, "B": "g4Y7+ew^.v", "C": "NK