From ee05fab1ce48a88b041a751c3ebce2c027b5ebcb Mon Sep 17 00:00:00 2001 From: prvyk Date: Wed, 21 May 2025 14:34:19 +0300 Subject: [PATCH 1/6] Fix CanDoSRANDMEMBERWithCountCommandLC test Fix remaining Encoding.ASCII.GetString usages. --- test/Garnet.test/GarnetServerConfigTests.cs | 1 + .../RespBlockingCollectionTests.cs | 6 +-- test/Garnet.test/RespSetTest.cs | 43 +++++++------------ test/Garnet.test/RespTests.cs | 3 +- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/test/Garnet.test/GarnetServerConfigTests.cs b/test/Garnet.test/GarnetServerConfigTests.cs index 27bda7b2b89..c6831de8fb7 100644 --- a/test/Garnet.test/GarnetServerConfigTests.cs +++ b/test/Garnet.test/GarnetServerConfigTests.cs @@ -866,6 +866,7 @@ public async Task MultiTcpSocketTest() var hostname = TestUtils.GetHostName(); var addresses = Dns.GetHostAddresses(hostname); addresses = [.. addresses, IPAddress.IPv6Loopback, IPAddress.Loopback]; + addresses = [.. addresses.Distinct()]; var endpoints = addresses.Select(address => new IPEndPoint(address, TestUtils.TestPort)).ToArray(); var server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, endpoints: endpoints); diff --git a/test/Garnet.test/RespBlockingCollectionTests.cs b/test/Garnet.test/RespBlockingCollectionTests.cs index c65436a969a..b87e1f86fcb 100644 --- a/test/Garnet.test/RespBlockingCollectionTests.cs +++ b/test/Garnet.test/RespBlockingCollectionTests.cs @@ -961,8 +961,7 @@ public async Task BasicBzmpopWithExpireItemsTest(string mode) using var lcr = TestUtils.CreateRequest(); var response = lcr.SendCommand($"BZMPOP 1 1 {key} {mode}"); var expectedResponse = "$-1\r\n"; - var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, actualValue); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); } [Test] @@ -1079,8 +1078,7 @@ public async Task BasicBzpopMinMaxWithExpireItemsTest(string command) using var lcr = TestUtils.CreateRequest(); var response = lcr.SendCommand($"{command} {key} 1"); var expectedResponse = "$-1\r\n"; - var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, actualValue); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); } [Test] diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 171f1d59ed8..267f66fe7af 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Garnet.common; using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -1007,7 +1008,7 @@ public void CanDoSCARDCommandsLC() } [Test] - public void CanDoSRANDMEMBERWithCountCommandLC() + public unsafe void CanDoSRANDMEMBERWithCountCommandLC() { var myset = new HashSet { "one", "two", "three", "four", "five" }; @@ -1025,43 +1026,31 @@ public void CanDoSRANDMEMBERWithCountCommandLC() CreateLongSet(); response = lightClientRequest.SendCommand("SRANDMEMBER myset", 1); - var strLen = Encoding.ASCII.GetString(response).Substring(1, 1); - var item = Encoding.ASCII.GetString(response).Substring(4, Int32.Parse(strLen)); + var strLen = Encoding.ASCII.GetString(response, 1, 1); + var item = Encoding.ASCII.GetString(response, 4, int.Parse(strLen)); ClassicAssert.IsTrue(myset.Contains(item)); // Get three random members response = lightClientRequest.SendCommand("SRANDMEMBER myset 3", 3); - TestUtils.AssertEqualUpToExpectedLength("*", response); - - var strResponse = Encoding.ASCII.GetString(response); - var arrLenEndIdx = strResponse.IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); - ClassicAssert.IsTrue(arrLenEndIdx > 1); - - var strArrLen = strResponse.AsSpan().Slice(1, arrLenEndIdx - 1); - ClassicAssert.IsTrue(int.TryParse(strArrLen, out var arrLen)); - ClassicAssert.AreEqual(3, arrLen); + TestUtils.AssertEqualUpToExpectedLength("*3\r\n", response); // Get 6 random members and verify that at least two elements are the same response = lightClientRequest.SendCommand("SRANDMEMBER myset -6", 6); - var strReponse = Encoding.ASCII.GetString(response); - arrLenEndIdx = strReponse.IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); - strArrLen = strReponse.AsSpan().Slice(1, arrLenEndIdx - 1); - ClassicAssert.IsTrue(int.TryParse(strArrLen, out arrLen)); + TestUtils.AssertEqualUpToExpectedLength("*6\r\n", response); + + string[] results; - var members = new HashSet(); - var repeatedMembers = false; - for (var i = 0; i < arrLen; i++) + fixed (byte* p = &response[0]) { - var member = strReponse.Substring(arrLenEndIdx + 2, response.Length - arrLenEndIdx - 5); - if (members.Contains(member)) - { - repeatedMembers = true; - break; - } - members.Add(member); + var ptr = p; + ClassicAssert.IsTrue( + RespReadUtils.TryReadStringArrayWithLengthHeader(out results, ref ptr, + p + (Garnet.common.NumUtils.MaximumFormatInt64Length * 10)) + ); } - ClassicAssert.IsTrue(repeatedMembers, "At least two members are repeated."); + ClassicAssert.IsTrue(results.Distinct().Count() != results.Length, + "At least two members are repeated."); } [Test] diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 0b766bc4bcc..2a11323747b 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -5024,7 +5024,7 @@ public async Task ClientUnblockBasicTest(string clientType, string mode, bool ex { var startTime = Stopwatch.GetTimestamp(); var response = blockingClient.SendCommand("BLMPOP 10 1 keyA LEFT"); - if (Encoding.ASCII.GetString(response).Substring(0, "-UNBLOCKED".Length) == "-UNBLOCKED") + if (Encoding.ASCII.GetString(response, 0, "-UNBLOCKED".Length) == "-UNBLOCKED") { isError = true; } @@ -5077,6 +5077,7 @@ public async Task MultipleClientsUnblockAndAddTest(int numberOfItems) using var blockingClient = TestUtils.CreateRequest(); var clientIdResponse = Encoding.ASCII.GetString(blockingClient.SendCommand("CLIENT ID")); var clientId = clientIdResponse.Substring(1, clientIdResponse.IndexOf("\r\n") - 1); + ClassicAssert.IsTrue(long.TryParse(clientId, out _)); string blockingResult = null; var blockingTask = Task.Run(() => From 06196191db2f515cd341a94d34ab22850af6437a Mon Sep 17 00:00:00 2001 From: prvyk Date: Wed, 21 May 2025 15:06:41 +0300 Subject: [PATCH 2/6] Convert test to GarnetClientSession. --- test/Garnet.test/RespSetTest.cs | 43 ++++++++++++--------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 267f66fe7af..e9437c6dfd2 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Garnet.common; using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -1008,46 +1007,36 @@ public void CanDoSCARDCommandsLC() } [Test] - public unsafe void CanDoSRANDMEMBERWithCountCommandLC() + public async Task CanDoSRANDMEMBERWithCountCommandLC() { var myset = new HashSet { "one", "two", "three", "four", "five" }; + using var c = TestUtils.GetGarnetClientSession(raw: true); + c.Connect(); + // Check SRANDMEMBER with non-existing key - using var lightClientRequest = TestUtils.CreateRequest(); - var response = lightClientRequest.SendCommand("SRANDMEMBER myset"); - var expectedResponse = "$-1\r\n"; - TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + var response = await c.ExecuteAsync("SRANDMEMBER", "myset"); + ClassicAssert.AreEqual("$-1\r\n", response); // Check SRANDMEMBER with non-existing key and count - response = lightClientRequest.SendCommand("SRANDMEMBER myset 3"); - expectedResponse = "*0\r\n"; - TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + response = await c.ExecuteAsync("SRANDMEMBER", "myset", "3"); + ClassicAssert.AreEqual("*0\r\n", response); CreateLongSet(); - response = lightClientRequest.SendCommand("SRANDMEMBER myset", 1); - var strLen = Encoding.ASCII.GetString(response, 1, 1); - var item = Encoding.ASCII.GetString(response, 4, int.Parse(strLen)); + c.RawResult = false; + var item = await c.ExecuteAsync("SRANDMEMBER", "myset"); ClassicAssert.IsTrue(myset.Contains(item)); // Get three random members - response = lightClientRequest.SendCommand("SRANDMEMBER myset 3", 3); - TestUtils.AssertEqualUpToExpectedLength("*3\r\n", response); + var results = await c.ExecuteForArrayAsync("SRANDMEMBER", "myset", "3"); + ClassicAssert.AreEqual(3, results.Length); + ClassicAssert.IsTrue(results.All(myset.Contains)); // Get 6 random members and verify that at least two elements are the same - response = lightClientRequest.SendCommand("SRANDMEMBER myset -6", 6); - TestUtils.AssertEqualUpToExpectedLength("*6\r\n", response); - - string[] results; - - fixed (byte* p = &response[0]) - { - var ptr = p; - ClassicAssert.IsTrue( - RespReadUtils.TryReadStringArrayWithLengthHeader(out results, ref ptr, - p + (Garnet.common.NumUtils.MaximumFormatInt64Length * 10)) - ); - } + results = await c.ExecuteForArrayAsync("SRANDMEMBER", "myset", "-6"); + ClassicAssert.AreEqual(6, results.Length); + ClassicAssert.IsTrue(results.All(myset.Contains)); ClassicAssert.IsTrue(results.Distinct().Count() != results.Length, "At least two members are repeated."); From 27be6eb083a1c9cc626f5a393ad888b2f0455e1a Mon Sep 17 00:00:00 2001 From: prvyk Date: Wed, 4 Jun 2025 17:18:48 +0300 Subject: [PATCH 3/6] Go back to lightClient. Add a simple RESP parsing class to verify result. --- test/Garnet.test/Resp/TestSimpleReadRESP.cs | 148 ++++++++++++++++++++ test/Garnet.test/RespSetTest.cs | 34 ++--- 2 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 test/Garnet.test/Resp/TestSimpleReadRESP.cs diff --git a/test/Garnet.test/Resp/TestSimpleReadRESP.cs b/test/Garnet.test/Resp/TestSimpleReadRESP.cs new file mode 100644 index 00000000000..3795fdbd84a --- /dev/null +++ b/test/Garnet.test/Resp/TestSimpleReadRESP.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Garnet.common.Parsing; + +namespace Garnet.test +{ + public static class TestSimpleReadRESP + { + public static object[] ReadRESP(byte[] inputArray) + { + var pos = 0; + var input = Encoding.ASCII.GetString(inputArray); + + return Read(input, ref pos); + } + + public static object[] ReadRESP(string input) + { + var pos = 0; + return Read(input, ref pos); + } + + static object[] Read(string input, ref int pos) + { + if (input.Length < 3) + return default; + + switch (input[pos]) + { + case '+': + pos++; + var resultString = ReadSimpleString(input, ref pos); + return [resultString]; + + case ':': + pos++; + var resultInt = ReadIntegerAsString(input, ref pos); + return [resultInt]; + + case '-': + pos++; + var errorString = ReadErrorAsString(input, ref pos); + return [errorString]; + + case '$': + pos++; + var resultBulk = ReadStringWithLengthHeader(input, ref pos); + return [resultBulk]; + + case '*': + pos++; + var resultArray = ReadStringArrayWithLengthHeader(input, ref pos); + return resultArray; + + default: + RespParsingException.Throw($"Unexpected character {input[0]}"); + throw new NotImplementedException(); + } + } + + private static object[] ReadStringArrayWithLengthHeader(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var arraylen = int.Parse(input[loc..pos]); + loc = pos + 2; + + if (arraylen < 0) + RespParsingException.ThrowInvalidLength(arraylen); + + List lo = new(); + for (var i = 0; i < arraylen; i++) + { + var res = Read(input, ref loc); + lo.AddRange(res); + } + + return [.. lo]; + } + + private static string ReadStringWithLengthHeader(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var len = int.Parse(input[loc..pos]); + if (len < -1) + RespParsingException.ThrowInvalidStringLength(len); + + loc = input.IndexOf("\r\n", pos + 2) + 2; + if (loc < 0) + RespParsingException.Throw("No newline!"); + + if (len == -1) + { + return null; + } + + if (loc != pos + 2 + len + 2) + RespParsingException.Throw("Invalid length!"); + + return input[(pos + 2)..(pos + 2 + len)]; + } + + private static string ReadErrorAsString(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var ret = input[loc..pos]; + loc = pos + 2; + + return ret; + } + + private static long ReadIntegerAsString(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var ret = long.Parse(input[loc..pos]); + loc = pos + 2; + + return ret; + } + + private static string ReadSimpleString(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var ret = input[loc..pos]; + loc = pos + 2; + + return ret; + } + } +} \ No newline at end of file diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index e9437c6dfd2..60465a59f6c 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -1007,37 +1007,39 @@ public void CanDoSCARDCommandsLC() } [Test] - public async Task CanDoSRANDMEMBERWithCountCommandLC() + public unsafe void CanDoSRANDMEMBERWithCountCommandLC() { var myset = new HashSet { "one", "two", "three", "four", "five" }; - using var c = TestUtils.GetGarnetClientSession(raw: true); - c.Connect(); - // Check SRANDMEMBER with non-existing key - var response = await c.ExecuteAsync("SRANDMEMBER", "myset"); - ClassicAssert.AreEqual("$-1\r\n", response); + using var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand("SRANDMEMBER myset"); + var expectedResponse = "$-1\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); // Check SRANDMEMBER with non-existing key and count - response = await c.ExecuteAsync("SRANDMEMBER", "myset", "3"); - ClassicAssert.AreEqual("*0\r\n", response); + response = lightClientRequest.SendCommand("SRANDMEMBER myset 3"); + expectedResponse = "*0\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); CreateLongSet(); - c.RawResult = false; - var item = await c.ExecuteAsync("SRANDMEMBER", "myset"); + response = lightClientRequest.SendCommand("SRANDMEMBER myset", 1); + var strLen = Encoding.ASCII.GetString(response, 1, 1); + var item = Encoding.ASCII.GetString(response, 4, int.Parse(strLen)); ClassicAssert.IsTrue(myset.Contains(item)); // Get three random members - var results = await c.ExecuteForArrayAsync("SRANDMEMBER", "myset", "3"); - ClassicAssert.AreEqual(3, results.Length); - ClassicAssert.IsTrue(results.All(myset.Contains)); + response = lightClientRequest.SendCommand("SRANDMEMBER myset 3", 3); + TestUtils.AssertEqualUpToExpectedLength("*3\r\n", response); // Get 6 random members and verify that at least two elements are the same - results = await c.ExecuteForArrayAsync("SRANDMEMBER", "myset", "-6"); - ClassicAssert.AreEqual(6, results.Length); - ClassicAssert.IsTrue(results.All(myset.Contains)); + response = lightClientRequest.SendCommand("SRANDMEMBER myset -6", 6); + TestUtils.AssertEqualUpToExpectedLength("*6\r\n", response); + + var results = TestSimpleReadRESP.ReadRESP(response); + ClassicAssert.IsTrue(results.All(a => myset.Contains((string)a))); ClassicAssert.IsTrue(results.Distinct().Count() != results.Length, "At least two members are repeated."); } From 5e187e2c903c5469cd0fe5862e3ae9d77c961350 Mon Sep 17 00:00:00 2001 From: prvyk Date: Thu, 5 Jun 2025 19:49:07 +0300 Subject: [PATCH 4/6] Add some RESP3 types to simple reader. --- test/Garnet.test/Resp/TestSimpleReadRESP.cs | 44 ++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/test/Garnet.test/Resp/TestSimpleReadRESP.cs b/test/Garnet.test/Resp/TestSimpleReadRESP.cs index 3795fdbd84a..7f15de19157 100644 --- a/test/Garnet.test/Resp/TestSimpleReadRESP.cs +++ b/test/Garnet.test/Resp/TestSimpleReadRESP.cs @@ -26,27 +26,16 @@ public static object[] ReadRESP(string input) static object[] Read(string input, ref int pos) { - if (input.Length < 3) - return default; - switch (input[pos]) { - case '+': - pos++; - var resultString = ReadSimpleString(input, ref pos); - return [resultString]; - case ':': pos++; var resultInt = ReadIntegerAsString(input, ref pos); return [resultInt]; - case '-': - pos++; - var errorString = ReadErrorAsString(input, ref pos); - return [errorString]; - case '$': + case '~': + case '%': pos++; var resultBulk = ReadStringWithLengthHeader(input, ref pos); return [resultBulk]; @@ -56,6 +45,21 @@ static object[] Read(string input, ref int pos) var resultArray = ReadStringArrayWithLengthHeader(input, ref pos); return resultArray; + case '+': + pos++; + var resultString = ReadSimpleString(input, ref pos); + return [resultString]; + + case '-': + pos++; + var errorString = ReadErrorAsString(input, ref pos); + return [errorString]; + + case ',': + pos++; + var resultDouble = ReadDoubleAsString(input, ref pos); + return [resultDouble]; + default: RespParsingException.Throw($"Unexpected character {input[0]}"); throw new NotImplementedException(); @@ -74,7 +78,7 @@ private static object[] ReadStringArrayWithLengthHeader(string input, ref int lo if (arraylen < 0) RespParsingException.ThrowInvalidLength(arraylen); - List lo = new(); + List lo = new(arraylen); for (var i = 0; i < arraylen; i++) { var res = Read(input, ref loc); @@ -120,6 +124,18 @@ private static string ReadErrorAsString(string input, ref int loc) return ret; } + + private static double ReadDoubleAsString(string input, ref int loc) + { + var pos = input.IndexOf("\r\n", loc); + if (pos == -1) + RespParsingException.Throw("No newline"); + + var ret = double.Parse(input[loc..pos]); + loc = pos + 2; + + return ret; + } private static long ReadIntegerAsString(string input, ref int loc) { From 7262757be4eb8db8c5cbf8b44c542d490bc86722 Mon Sep 17 00:00:00 2001 From: prvyk Date: Fri, 6 Jun 2025 13:51:30 +0300 Subject: [PATCH 5/6] fmt --- test/Garnet.test/Resp/TestSimpleReadRESP.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/Resp/TestSimpleReadRESP.cs b/test/Garnet.test/Resp/TestSimpleReadRESP.cs index 7f15de19157..0940cb1ac2f 100644 --- a/test/Garnet.test/Resp/TestSimpleReadRESP.cs +++ b/test/Garnet.test/Resp/TestSimpleReadRESP.cs @@ -124,7 +124,7 @@ private static string ReadErrorAsString(string input, ref int loc) return ret; } - + private static double ReadDoubleAsString(string input, ref int loc) { var pos = input.IndexOf("\r\n", loc); From df30b6b25f58908d4bd898fca4adc0c24be6753f Mon Sep 17 00:00:00 2001 From: prvyk Date: Fri, 6 Jun 2025 14:15:30 +0300 Subject: [PATCH 6/6] No need for unsafe now. --- test/Garnet.test/RespSetTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 60465a59f6c..c8d6647b14a 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -1007,7 +1007,7 @@ public void CanDoSCARDCommandsLC() } [Test] - public unsafe void CanDoSRANDMEMBERWithCountCommandLC() + public void CanDoSRANDMEMBERWithCountCommandLC() { var myset = new HashSet { "one", "two", "three", "four", "five" }; @@ -1041,7 +1041,7 @@ public unsafe void CanDoSRANDMEMBERWithCountCommandLC() ClassicAssert.IsTrue(results.All(a => myset.Contains((string)a))); ClassicAssert.IsTrue(results.Distinct().Count() != results.Length, - "At least two members are repeated."); + "At least two members must be repeated."); } [Test]