From 2dab5538977237302e63f0cd0d7b183bf9ee1a30 Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Mon, 26 Jun 2023 12:39:26 +0300 Subject: [PATCH 1/8] added deserialization parts of converters for some tarantool msgpack ext types: guids, decimals, datetime. added integration tests for deserialization for all supported data types --- .../Converters/DateTimeConverter.cs | 64 +++++ .../Converters/DecimalConverter.cs | 193 ++++++++++++++ .../Converters/GuidConverter.cs | 49 ++++ .../Model/Enums/MsgPackExtDataTypes.cs | 16 ++ .../TarantoolConvertersRegistrator.cs | 6 + .../Utils/ExceptionHelper.cs | 10 + src/tests/docker-compose/docker-compose.yml | 24 ++ .../tarantool/tarantool.docker.lua | 7 + .../DataTypes/DeserializationTests.cs | 239 ++++++++++++++++++ .../TarantoolBaseTest.cs | 26 ++ ...rogaudi.tarantool.integration.tests.csproj | 29 +++ 11 files changed, 663 insertions(+) create mode 100644 src/progaudi.tarantool/Converters/DateTimeConverter.cs create mode 100644 src/progaudi.tarantool/Converters/DecimalConverter.cs create mode 100644 src/progaudi.tarantool/Converters/GuidConverter.cs create mode 100644 src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs create mode 100644 src/tests/docker-compose/docker-compose.yml create mode 100644 src/tests/docker-compose/tarantool/tarantool.docker.lua create mode 100644 src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs create mode 100644 src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs create mode 100644 src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj diff --git a/src/progaudi.tarantool/Converters/DateTimeConverter.cs b/src/progaudi.tarantool/Converters/DateTimeConverter.cs new file mode 100644 index 00000000..65b8d2e9 --- /dev/null +++ b/src/progaudi.tarantool/Converters/DateTimeConverter.cs @@ -0,0 +1,64 @@ +using ProGaudi.MsgPack.Light; +using ProGaudi.Tarantool.Client.Model.Enums; +using ProGaudi.Tarantool.Client.Utils; +using System; +using System.Buffers.Binary; + +namespace ProGaudi.Tarantool.Client.Converters +{ + /// + /// Converter for Tarantool datetime values, implemeted as MsgPack extension. + /// See https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type + /// + internal class DateTimeConverter : IMsgPackConverter, IMsgPackConverter + { + private const byte MP_DATETIME = 0x04; + private static readonly DateTime UnixEpocUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public void Initialize(MsgPackContext context) + { + } + + public DateTime Read(IMsgPackReader reader) + { + var dataType = reader.ReadByte(); + var mpHeader = reader.ReadByte(); + if (mpHeader != MP_DATETIME) + { + throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_DATETIME); + } + + if (dataType == MsgPackExtDataTypes.FixExt8) + { + var seconds = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4)); + var nanoSeconds = BinaryPrimitives.ReadInt16LittleEndian(reader.ReadBytes(2)); + var _ = reader.ReadBytes(2);// also need to extract tzoffset; tzindex; + return UnixEpocUtc.AddSeconds(seconds).AddTicks(nanoSeconds / 100); + } + else if (dataType == MsgPackExtDataTypes.FixExt16) + { + var seconds = BinaryPrimitives.ReadInt64LittleEndian(reader.ReadBytes(8)); + var nanoSeconds = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4)); + var _ = reader.ReadBytes(4);// also need to extract tzoffset; tzindex; + return UnixEpocUtc.AddSeconds(seconds).AddTicks(nanoSeconds / 100); + } + + throw ExceptionHelper.UnexpectedDataType(dataType, MsgPackExtDataTypes.FixExt8, MsgPackExtDataTypes.FixExt16); + } + + DateTimeOffset IMsgPackConverter.Read(IMsgPackReader reader) + { + return Read(reader); + } + + public void Write(DateTimeOffset value, IMsgPackWriter writer) + { + throw new NotImplementedException(); + } + + public void Write(DateTime value, IMsgPackWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/progaudi.tarantool/Converters/DecimalConverter.cs b/src/progaudi.tarantool/Converters/DecimalConverter.cs new file mode 100644 index 00000000..833ae406 --- /dev/null +++ b/src/progaudi.tarantool/Converters/DecimalConverter.cs @@ -0,0 +1,193 @@ +using ProGaudi.MsgPack.Light; +using ProGaudi.Tarantool.Client.Model.Enums; +using ProGaudi.Tarantool.Client.Utils; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace ProGaudi.Tarantool.Client.Converters +{ + /// + /// Format described in https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type + /// + internal class DecimalConverter : IMsgPackConverter + { + private static readonly byte[] SupportedFixTypes = new byte[5] + { + MsgPackExtDataTypes.FixExt1, + MsgPackExtDataTypes.FixExt2, + MsgPackExtDataTypes.FixExt4, + MsgPackExtDataTypes.FixExt8, + MsgPackExtDataTypes.FixExt16 + }; + private static readonly byte[] SupportedNonFixTypes = new byte[3] + { + MsgPackExtDataTypes.Ext8, + MsgPackExtDataTypes.Ext16, + MsgPackExtDataTypes.Ext32 + }; + + private const byte MP_DECIMAL = 0x01; + private const byte DECIMAL_MINUS = 0x0D; + private const byte DECIMAL_MINUS_ALT = 0x0B; + + private IMsgPackConverter doubleConverter; + + public void Initialize(MsgPackContext context) + { + doubleConverter = context.GetConverter(); + } + + public decimal Read(IMsgPackReader reader) + { + var dataType = reader.ReadByte(); + var fixedDataType = true; + var len = 0; + switch (dataType) + { + case MsgPackExtDataTypes.Ext8: + case MsgPackExtDataTypes.Ext16: + case MsgPackExtDataTypes.Ext32: + fixedDataType = false; + break; + case MsgPackExtDataTypes.FixExt1: + len = 1; + break; + case MsgPackExtDataTypes.FixExt2: + len = 2; + break; + case MsgPackExtDataTypes.FixExt4: + len = 4; + break; + case MsgPackExtDataTypes.FixExt8: + len = 8; + break; + case MsgPackExtDataTypes.FixExt16: + len = 16; + break; + default: + throw ExceptionHelper.UnexpectedDataType(dataType, SupportedFixTypes.Union(SupportedNonFixTypes).ToArray()); + } + + if (!fixedDataType) + { + len = reader.ReadByte(); + } + + var mpHeader = reader.ReadByte(); + if (mpHeader != MP_DECIMAL) + { + throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_DECIMAL); + } + + var data = reader.ReadBytes((uint)len).ToArray(); + + // see https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java + + // Extract sign from the last nibble + int signum = (byte)(SecondNibbleFromByte(data[len - 1])); + if (signum == DECIMAL_MINUS || signum == DECIMAL_MINUS_ALT) + { + signum = -1; + } + else if (signum <= 0x09) + { + throw new IOException("The sign nibble has wrong value"); + } + else + { + signum = 1; + } + + int scale = data[0]; + int skipIndex = 1; //skip byte with scale + + int digitsNum = (len - skipIndex) << 1; + char digit = Digit(FirstNibbleFromByte(data[len - 1]), digitsNum - 1); + + char[] digits = new char[digitsNum]; + int pos = 2 * (len - skipIndex) - 1; + + digits[pos--] = digit; + for (int i = len - 2; i >= skipIndex; i--) + { + digits[pos--] = Digit(SecondNibbleFromByte(data[i]), pos); + digits[pos--] = Digit(FirstNibbleFromByte(data[i]), pos); + } + + return CreateDecimalFromDigits(digits, scale, signum < 0); + } + + public void Write(decimal value, IMsgPackWriter writer) + { + throw new NotImplementedException(); + } + + private static int UnsignedRightShift(int signed, int places) + { + unchecked + { + var unsigned = (uint)signed; + unsigned >>= places; + return (int)unsigned; + } + } + + private static int FirstNibbleFromByte(byte val) + { + return UnsignedRightShift(val & 0xF0, 4); + } + + private static int SecondNibbleFromByte(byte val) + { + return val & 0x0F; + } + + private static char Digit(int val, int pos) + { + var digit = (char)val; + if (digit > 9) + { + throw new IOException(String.Format("Invalid digit at position %d", pos)); + } + return digit; + } + + private static decimal CreateDecimalFromDigits(char[] digits, int scale, bool isNegative) + { + int pos = 0; + while (pos < digits.Length && digits[pos] == 0) + { + pos++; + } + + if (pos == digits.Length) + { + return 0; + } + + StringBuilder sb = new StringBuilder(); + for (; pos < digits.Length; pos++) + { + sb.Append((int)digits[pos]); + } + + if (scale >= sb.Length) + { + sb.Insert(0, String.Join("", Enumerable.Range(0, scale - sb.Length + 1).Select(_ => "0"))); + } + + if (scale > 0) + { + sb.Insert(sb.Length - scale, "."); + } + + if (isNegative) + { + sb.Insert(0, '-'); + } + return Decimal.Parse(sb.ToString()); + } + } +} diff --git a/src/progaudi.tarantool/Converters/GuidConverter.cs b/src/progaudi.tarantool/Converters/GuidConverter.cs new file mode 100644 index 00000000..480de7e0 --- /dev/null +++ b/src/progaudi.tarantool/Converters/GuidConverter.cs @@ -0,0 +1,49 @@ +using ProGaudi.MsgPack.Light; +using ProGaudi.Tarantool.Client.Model.Enums; +using ProGaudi.Tarantool.Client.Utils; +using System; +using System.Buffers.Binary; +using System.Linq; + +namespace ProGaudi.Tarantool.Client.Converters +{ + /// + /// Converter for Tarantool uuid values, implemeted as MsgPack extension. + /// See https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type + /// + internal class GuidConverter : IMsgPackConverter + { + private static readonly byte GuidDataType = MsgPackExtDataTypes.FixExt16; + private const byte MP_UUID = 0x02; + + public void Initialize(MsgPackContext context) + { + } + + public Guid Read(IMsgPackReader reader) + { + var dataType = reader.ReadByte(); + if (dataType != GuidDataType) + { + throw ExceptionHelper.UnexpectedDataType(dataType, GuidDataType); + } + + var mpHeader = reader.ReadByte(); + if (mpHeader != MP_UUID) + { + throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_UUID); + } + + int intToken = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + short shortToken1 = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2)); + short shortToken2 = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2)); + + return new Guid(intToken, shortToken1, shortToken2, reader.ReadBytes(8).ToArray()); + } + + public void Write(Guid value, IMsgPackWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs b/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs new file mode 100644 index 00000000..58f0fcfe --- /dev/null +++ b/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs @@ -0,0 +1,16 @@ +namespace ProGaudi.Tarantool.Client.Model.Enums +{ + // probably the best decision is to move these values into DataTypes enum in MsgPack.Light.dll + // for now I decided not to touch it + internal class MsgPackExtDataTypes + { + public const byte Ext8 = 0xc7; + public const byte Ext16 = 0xc8; + public const byte Ext32 = 0xc9; + public const byte FixExt1 = 0xd4; + public const byte FixExt2 = 0xd5; + public const byte FixExt4 = 0xd6; + public const byte FixExt8 = 0xd7; + public const byte FixExt16 = 0xd8; + } +} diff --git a/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs b/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs index d2710f97..f42570ff 100644 --- a/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs +++ b/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs @@ -4,6 +4,7 @@ using ProGaudi.Tarantool.Client.Model; using ProGaudi.Tarantool.Client.Model.Enums; using ProGaudi.Tarantool.Client.Model.Responses; +using System; namespace ProGaudi.Tarantool.Client { @@ -52,6 +53,11 @@ public static void Register(MsgPackContext context) context.RegisterConverter(new PingPacketConverter()); context.RegisterConverter(new ExecuteSqlRequestConverter()); + context.RegisterConverter(new DecimalConverter()); + context.RegisterConverter(new GuidConverter()); + context.RegisterConverter(new DateTimeConverter()); + context.RegisterConverter(new DateTimeConverter()); + context.RegisterGenericConverter(typeof(TupleConverter<>)); context.RegisterGenericConverter(typeof(TupleConverter<,>)); context.RegisterGenericConverter(typeof(TupleConverter<,,>)); diff --git a/src/progaudi.tarantool/Utils/ExceptionHelper.cs b/src/progaudi.tarantool/Utils/ExceptionHelper.cs index c2b4df3e..2766b9c0 100644 --- a/src/progaudi.tarantool/Utils/ExceptionHelper.cs +++ b/src/progaudi.tarantool/Utils/ExceptionHelper.cs @@ -37,6 +37,16 @@ public static Exception UnexpectedDataType(DataTypes expected, DataTypes actual) return new ArgumentException($"Unexpected data type: {expected} is expected, but got {actual}."); } + public static Exception UnexpectedDataType(byte actualCode, params byte[] expectedCodes) + { + return new ArgumentException($"Unexpected data type: {String.Join(", ", expectedCodes)} is expected, but got {actualCode}."); + } + + public static Exception UnexpectedMsgPackHeader(byte actual, byte expected) + { + return new ArgumentException($"Unexpected msgpack header: {expected} is expected, but got {actual}."); + } + public static Exception NotConnected() { return new InvalidOperationException("Can't perform operation. Looks like we are not connected to tarantool. Call 'Connect' method before calling any other operations."); diff --git a/src/tests/docker-compose/docker-compose.yml b/src/tests/docker-compose/docker-compose.yml new file mode 100644 index 00000000..96bd4281 --- /dev/null +++ b/src/tests/docker-compose/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.2' + +services: + tarantool_2_10: + image: tarantool/tarantool:2.10 + command: tarantool /usr/local/share/tarantool/tarantool.docker.lua + volumes: + - ./tarantool:/usr/local/share/tarantool + ports: + - "3310:3301" + environment: + TARANTOOL_USER_NAME: admin + TARANTOOL_USER_PASSWORD: adminPassword + + tarantool_2_11: + image: tarantool/tarantool:2.11 + command: tarantool /usr/local/share/tarantool/tarantool.docker.lua + volumes: + - ./tarantool:/usr/local/share/tarantool + ports: + - "3311:3301" + environment: + TARANTOOL_USER_NAME: admin + TARANTOOL_USER_PASSWORD: adminPassword diff --git a/src/tests/docker-compose/tarantool/tarantool.docker.lua b/src/tests/docker-compose/tarantool/tarantool.docker.lua new file mode 100644 index 00000000..fd40a404 --- /dev/null +++ b/src/tests/docker-compose/tarantool/tarantool.docker.lua @@ -0,0 +1,7 @@ +box.cfg +{ + pid_file = nil, + background = false, + log_level = 5, + listen = 3301 +} \ No newline at end of file diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs new file mode 100644 index 00000000..bf6eb7f7 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs @@ -0,0 +1,239 @@ +using Xunit; +using Shouldly; +using System.Globalization; +using System; + +namespace progaudi.tarantool.integration.tests.DataTypes +{ + // X - nil + // X - boolean + // X - integer + // X - float64 (double) + // ??? binary + // (deserialize only yet, partially for ext type, not fixext type) decimal + // X (deserialize only yet) - datetime + // (can't use C# TimeSpan for deserialization, because tarantool's interval can contains years and months) interval + // X (deserialize only yet) - uuid + // X map + // X array + + /// + /// Test suite, where we create and return some values in Tarantool via Lua and Eval command, + /// and check that this value deserialize into correspoding C# class/structure correctly + /// + public class DeserializationTests : TarantoolBaseTest + { + [Fact] + public async Task DeserializeNil_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval("return box.NULL"); + result.Data.Length.ShouldBe(1); + result.Data[0].ShouldBeNull(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DeserializeBoolean_ShouldBeCorrectAsync(bool val) + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval($"return {(val ? "true" : "false")}"); + result.Data.Length.ShouldBe(1); + result.Data[0].ShouldBe(val); + } + + [Theory] + [InlineData(1)] + [InlineData(0)] + [InlineData(-1)] + public async Task DeserializeInt_ShouldBeCorrectAsync(int val) + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval($"return {val}"); + result.Data.ShouldBe(new[] { val }); + } + + [Fact] + public async Task DeserializeFloat64_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval("return math.sqrt(2)"); + result.Data.Length.ShouldBe(1); + Math.Abs(result.Data[0] - Math.Sqrt(2)).ShouldBeLessThan(double.Epsilon); + } + + [Fact] + public async Task DeserializeString_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var expectedStr = "Tarantool tickles, makes spiders giggle"; + var result = await tarantoolClient.Eval($"return '{expectedStr}'"); + result.Data.ShouldBe(new[] { expectedStr }); + } + + [Theory] + [InlineData("2.7182818284590452353602874714")] + [InlineData("2.718281828459045235360287471")] + [InlineData("2.71828182845904523536028747")] + [InlineData("2.7182818284590452353602874")] + [InlineData("2.718281828459045235360287")] + [InlineData("2.71828182845904523536028")] + [InlineData("2.7182818284590452353602")] + [InlineData("2.718281828459045235360")] + [InlineData("2.71828182845904523536")] + [InlineData("2.7182818284590452353")] + [InlineData("2.718281828459045235")] + [InlineData("2.71828182845904523")] + [InlineData("2.7182818284590452")] + [InlineData("2.718281828459045")] + [InlineData("2.71828182845904")] + [InlineData("2.7182818284590")] + [InlineData("2.718281828459")] + [InlineData("2.71828182845")] + [InlineData("2.7182818284")] + [InlineData("2.718281828")] + [InlineData("2.71828182")] + [InlineData("2.7182818")] + [InlineData("2.718281")] + [InlineData("2.71828")] + [InlineData("2.7182")] + [InlineData("2.718")] + [InlineData("2.71")] + [InlineData("2.7")] + [InlineData("2")] + [InlineData("0")] + [InlineData("100000")] + [InlineData("0.1")] + [InlineData("0.01")] + [InlineData("0.001")] + [InlineData("0.0001")] + public async Task DeserializeExtAsDecimal_ShouldBeCorrectAsync(string str) + { + decimal n = Decimal.Parse(str); + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{n}\")"); + result.Data.ShouldBe(new[] { n }); + + var negativeResult = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{-n}\")"); + negativeResult.Data.ShouldBe(new[] { -n }); + } + + //[Fact] + //does it expected behaviour? + public async Task DeserializeNumberAsDecimal_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + const decimal number = 1.23456789m; + var result = await tarantoolClient.Eval($"return {number}"); + result.Data.ShouldBe(new[] { number }); + } + + [Fact] + public async Task DeserializeExtAsGuid_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var guid = new Guid(); + var result = await tarantoolClient.Eval($"local uuid = require(\"uuid\"); return uuid.fromstr(\"{guid}\")"); + result.Data.ShouldBe(new[] { guid }); + } + + [Fact] + public async Task DeserializeExtAsDatetime_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var dt = DateTime.UtcNow; + var query = $"local dt = require(\"datetime\"); " + + $"return dt.new {{ msec = {dt.Millisecond}, sec = {dt.Second}, min = {dt.Minute}, hour = {dt.Hour}, day = {dt.Day}, month = {dt.Month}, year = {dt.Year} }}"; + // TODO: test tzoffset, nsec/usec additionally + var result = await tarantoolClient.Eval(query); + result.Data.Length.ShouldBe(1); + var actualDt = result.Data[0]; + actualDt.Date.ShouldBe(dt.Date); + actualDt.Hour.ShouldBe(dt.Hour); + actualDt.Minute.ShouldBe(dt.Minute); + actualDt.Second.ShouldBe(dt.Second); + actualDt.Millisecond.ShouldBe(dt.Millisecond); + + var resultOffset = await tarantoolClient.Eval(query); + resultOffset.Data.Length.ShouldBe(1); + var actualOffset = resultOffset.Data[0]; + actualDt.Date.ShouldBe(dt.Date); + actualDt.Hour.ShouldBe(dt.Hour); + actualDt.Minute.ShouldBe(dt.Minute); + actualDt.Second.ShouldBe(dt.Second); + actualDt.Millisecond.ShouldBe(dt.Millisecond); + } + + [Fact] + public async Task DeserializeIntArray_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var arr = new int[3] { 1, 2, 3}; + var result = await tarantoolClient.Eval($"return {{{String.Join(",", arr)}}}"); + result.Data.ShouldBe(new[] { arr }); + } + + [Fact] + public async Task DeserializeBooleanArray_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var arr = new bool[3] { false, true, false }; + var result = await tarantoolClient.Eval($"return {{{String.Join(",", arr.Select(x => x.ToString().ToLower()))}}}"); + result.Data.ShouldBe(new[] { arr }); + } + + [Fact] + public async Task DeserializeFloat64Array_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var arr = new double[3] { Math.Sqrt(2), Math.Sqrt(3), Math.Sqrt(5) }; + var result = await tarantoolClient.Eval("return { math.sqrt(2), math.sqrt(3), math.sqrt(5) }"); + result.Data.Length.ShouldBe(1); + result.Data[0].Length.ShouldBe(3); + Math.Abs(result.Data[0][0] - Math.Sqrt(2)).ShouldBeLessThan(double.Epsilon); + Math.Abs(result.Data[0][1] - Math.Sqrt(3)).ShouldBeLessThan(double.Epsilon); + Math.Abs(result.Data[0][2] - Math.Sqrt(5)).ShouldBeLessThan(double.Epsilon); + } + + [Fact] + public async Task DeserializeStringArray_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var arr = new string[3] { "foo", "bar", "foobar" }; + var result = await tarantoolClient.Eval($"return {{{String.Join(",", arr.Select(x => "'" + x + "'"))}}}"); + result.Data.ShouldBe(new[] { arr }); + } + + [Fact] + public async Task DeserializeMixedArrayToTuple_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval>("return { 1, true, 'foo'}"); + result.Data.Length.ShouldBe(1); + result.Data[0].Item1.ShouldBe(1); + result.Data[0].Item2.ShouldBe(true); + result.Data[0].Item3.ShouldBe("foo"); + } + + [Fact] + public async Task DeserializeMapToDictionary_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var expectedDict = new Dictionary() + { + { "foo", 1 }, + { "bar", 2 }, + { "baz", 3 } + }; + var result = await tarantoolClient.Eval>($"a = {{}}; {String.Join("; ", expectedDict.Select(kvp => $"a['{kvp.Key}']={kvp.Value}"))} return a"); + result.Data.Length.ShouldBe(1); + var actualDict = result.Data[0]; + foreach (var key in expectedDict.Keys) // order doesn't preserve, so we need to check key by key + { + actualDict.ContainsKey(key).ShouldBeTrue(); + actualDict[key].ShouldBe(expectedDict[key]); + } + } + } +} \ No newline at end of file diff --git a/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs b/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs new file mode 100644 index 00000000..fe0daa66 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs @@ -0,0 +1,26 @@ +using ProGaudi.Tarantool.Client; + +namespace progaudi.tarantool.integration.tests +{ + public class TarantoolBaseTest + { + public static async Task GetTarantoolClient(string userName = null, string password = null) + { + userName ??= "admin"; + password ??= "adminPassword"; + return await Box.Connect(BuildConnectionString(userName, password)); + } + + private static string BuildConnectionString(string userName, string password) + { + var userToken = (userName, password) + switch + { + (null, null) => "", + (_, null) => $"{userName}@", + _ => $"{userName}:{password}@", + }; + return $"{userToken}{Environment.GetEnvironmentVariable("TARANTOOL_HOST_FOR_TESTS") ?? "127.0.0.1:3310"}"; + } + } +} diff --git a/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj b/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj new file mode 100644 index 00000000..c0f2cdb6 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + From 1433308ed207a4a1bff70b5cff310bfbd08ebfab Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Tue, 27 Jun 2023 19:14:49 +0300 Subject: [PATCH 2/8] added deserialization tests for binary, some basic serialization tests --- src/tests/README.md | 10 ++++ .../DataTypes/DeserializationTests.cs | 47 ++++++++++--------- .../DataTypes/SerializationTests.cs | 45 ++++++++++++++++++ 3 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 src/tests/README.md create mode 100644 src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 00000000..78203d35 --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,10 @@ +To run integration tests you should have runned Tarantool instance. +You can run it locally via command + +docker-compose up -d + +It runs empty Tarantool instances of different versions, exposed on different ports. +Command examples to run integration tests suite for particular Tarantool instance + +dotnet test --filter "DisplayName~progaudi.tarantool.integration.tests" -e TARANTOOL_HOST_FOR_TESTS=127.0.0.1:3310 +dotnet test --filter "DisplayName~progaudi.tarantool.integration.tests" -e TARANTOOL_HOST_FOR_TESTS=127.0.0.1:3311 diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs index bf6eb7f7..77a6e576 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs @@ -1,22 +1,9 @@ using Xunit; using Shouldly; -using System.Globalization; -using System; +using System.Text; namespace progaudi.tarantool.integration.tests.DataTypes { - // X - nil - // X - boolean - // X - integer - // X - float64 (double) - // ??? binary - // (deserialize only yet, partially for ext type, not fixext type) decimal - // X (deserialize only yet) - datetime - // (can't use C# TimeSpan for deserialization, because tarantool's interval can contains years and months) interval - // X (deserialize only yet) - uuid - // X map - // X array - /// /// Test suite, where we create and return some values in Tarantool via Lua and Eval command, /// and check that this value deserialize into correspoding C# class/structure correctly @@ -24,7 +11,7 @@ namespace progaudi.tarantool.integration.tests.DataTypes public class DeserializationTests : TarantoolBaseTest { [Fact] - public async Task DeserializeNil_ShouldBeCorrectAsync() + public async Task DeserializeNull_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); var result = await tarantoolClient.Eval("return box.NULL"); @@ -32,6 +19,15 @@ public async Task DeserializeNil_ShouldBeCorrectAsync() result.Data[0].ShouldBeNull(); } + [Fact] + public async Task DeserializeNil_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval("return nil"); + result.Data.Length.ShouldBe(1); + result.Data[0].ShouldBeNull(); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -119,14 +115,23 @@ public async Task DeserializeExtAsDecimal_ShouldBeCorrectAsync(string str) negativeResult.Data.ShouldBe(new[] { -n }); } - //[Fact] - //does it expected behaviour? - public async Task DeserializeNumberAsDecimal_ShouldBeCorrectAsync() + [Fact] + public async Task DeserializeBinary8_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval($"local msgpack = require(\"msgpack\"); return msgpack.object_from_raw('\\xc4\\x06foobar')"); + var expectedByteArray = Encoding.ASCII.GetBytes("foobar"); + result.Data.ShouldBe(new[] { expectedByteArray }); + } + + [Fact] + public async Task DeserializeBinary16_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); - const decimal number = 1.23456789m; - var result = await tarantoolClient.Eval($"return {number}"); - result.Data.ShouldBe(new[] { number }); + var stringLen256 = String.Join("", Enumerable.Range(0, 256).Select(_ => "x")); + var result = await tarantoolClient.Eval($"local msgpack = require(\"msgpack\"); return msgpack.object_from_raw('\\xc5\\x01\\x00{stringLen256}')"); + var expectedByteArray = Encoding.ASCII.GetBytes(stringLen256); + result.Data.ShouldBe(new[] { expectedByteArray }); } [Fact] diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs new file mode 100644 index 00000000..e0244165 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs @@ -0,0 +1,45 @@ +using ProGaudi.Tarantool.Client.Model; +using Shouldly; +using Xunit; + +namespace progaudi.tarantool.integration.tests.DataTypes +{ + /// + /// Test suite, where we check correct data types serialization, when we pass values into Eval command, + /// and also check that this value deserialize into correspoding C# class/structure correctly + /// + public class SerializationTests : TarantoolBaseTest + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeBoolean_ShouldBeCorrectAsync(bool val) + { + await TestThatYouGetWhatYouGive(val); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(1000000)] + public async Task SerializeInt_ShouldBeCorrectAsync(int val) + { + await TestThatYouGetWhatYouGive(val); + } + + [Theory] + [InlineData("test")] + public async Task SerializeString_ShouldBeCorrectAsync(string val) + { + await TestThatYouGetWhatYouGive(val); + } + + private static async Task TestThatYouGetWhatYouGive(T val) + { + using var tarantoolClient = await GetTarantoolClient(); + var result = await tarantoolClient.Eval, T>($"return ...", TarantoolTuple.Create(val)); + result.Data.Length.ShouldBe(1); + result.Data[0].ShouldBe(val); + } + } +} From d3542c1fb6420bae529154467cf4545fc2c07290 Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Wed, 28 Jun 2023 18:45:55 +0300 Subject: [PATCH 3/8] added deserialization tests for decimal overflow, added serialization of guid, datetime, decimal and tests --- .../Converters/DateTimeConverter.cs | 27 +++++++- .../Converters/DecimalConverter.cs | 65 ++++++++++++++++--- .../Converters/GuidConverter.cs | 22 ++++++- .../DataTypes/DeserializationTests.cs | 19 +++++- .../DataTypes/SerializationTests.cs | 33 ++++++++-- 5 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src/progaudi.tarantool/Converters/DateTimeConverter.cs b/src/progaudi.tarantool/Converters/DateTimeConverter.cs index 65b8d2e9..c90d337f 100644 --- a/src/progaudi.tarantool/Converters/DateTimeConverter.cs +++ b/src/progaudi.tarantool/Converters/DateTimeConverter.cs @@ -53,12 +53,35 @@ DateTimeOffset IMsgPackConverter.Read(IMsgPackReader reader) public void Write(DateTimeOffset value, IMsgPackWriter writer) { - throw new NotImplementedException(); + var timeSpan = value.ToUniversalTime().Subtract(UnixEpocUtc); + long seconds = (long)timeSpan.TotalSeconds; + timeSpan = timeSpan.Subtract(TimeSpan.FromSeconds(seconds)); + int nanoSeconds = (int)(timeSpan.Ticks * 100); + int _ = 0;// also need to extract tzoffset; tzindex; + + writer.Write(MsgPackExtDataTypes.FixExt16); + writer.Write(MP_DATETIME); + + var byteArray = new byte[8]; + var span = new Span(byteArray); + BinaryPrimitives.WriteInt64LittleEndian(span, seconds); + writer.Write(byteArray); + + byteArray = new byte[4]; + span = new Span(byteArray); + BinaryPrimitives.WriteInt32LittleEndian(span, nanoSeconds); + writer.Write(byteArray); + + byteArray = new byte[4]; + span = new Span(byteArray); + BinaryPrimitives.WriteInt32LittleEndian(span, _); + writer.Write(byteArray); + } public void Write(DateTime value, IMsgPackWriter writer) { - throw new NotImplementedException(); + Write((DateTimeOffset)value, writer); } } } diff --git a/src/progaudi.tarantool/Converters/DecimalConverter.cs b/src/progaudi.tarantool/Converters/DecimalConverter.cs index 833ae406..399d8cc2 100644 --- a/src/progaudi.tarantool/Converters/DecimalConverter.cs +++ b/src/progaudi.tarantool/Converters/DecimalConverter.cs @@ -9,9 +9,11 @@ namespace ProGaudi.Tarantool.Client.Converters { /// + /// Converter for Tarantool decimal values, implemented as MsgPack extension. /// Format described in https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type + /// Limitation: .NET decimal max scale is 28 digits, when Tarantool decimal max scale is 38 digits /// - internal class DecimalConverter : IMsgPackConverter + public class DecimalConverter : IMsgPackConverter { private static readonly byte[] SupportedFixTypes = new byte[5] { @@ -29,14 +31,12 @@ internal class DecimalConverter : IMsgPackConverter }; private const byte MP_DECIMAL = 0x01; + private const byte DECIMAL_PLUS = 0x0C; private const byte DECIMAL_MINUS = 0x0D; private const byte DECIMAL_MINUS_ALT = 0x0B; - private IMsgPackConverter doubleConverter; - public void Initialize(MsgPackContext context) { - doubleConverter = context.GetConverter(); } public decimal Read(IMsgPackReader reader) @@ -83,7 +83,7 @@ public decimal Read(IMsgPackReader reader) var data = reader.ReadBytes((uint)len).ToArray(); - // see https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java + // used Java impl https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java // Extract sign from the last nibble int signum = (byte)(SecondNibbleFromByte(data[len - 1])); @@ -101,10 +101,15 @@ public decimal Read(IMsgPackReader reader) } int scale = data[0]; + if (scale > 28) + { + throw new OverflowException($"Maximum .NET decimal scale is exceeded. Maximum: 28. Actual: {scale}"); + } + int skipIndex = 1; //skip byte with scale int digitsNum = (len - skipIndex) << 1; - char digit = Digit(FirstNibbleFromByte(data[len - 1]), digitsNum - 1); + char digit = CharFromDigit(FirstNibbleFromByte(data[len - 1]), digitsNum - 1); char[] digits = new char[digitsNum]; int pos = 2 * (len - skipIndex) - 1; @@ -112,8 +117,8 @@ public decimal Read(IMsgPackReader reader) digits[pos--] = digit; for (int i = len - 2; i >= skipIndex; i--) { - digits[pos--] = Digit(SecondNibbleFromByte(data[i]), pos); - digits[pos--] = Digit(FirstNibbleFromByte(data[i]), pos); + digits[pos--] = CharFromDigit(SecondNibbleFromByte(data[i]), pos); + digits[pos--] = CharFromDigit(FirstNibbleFromByte(data[i]), pos); } return CreateDecimalFromDigits(digits, scale, signum < 0); @@ -121,7 +126,42 @@ public decimal Read(IMsgPackReader reader) public void Write(decimal value, IMsgPackWriter writer) { - throw new NotImplementedException(); + (int scale, decimal unscaledValue) = ExtractScaleFromDecimal(value); + + // used Java impl https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java + var unscaledValueStr = unscaledValue.ToString(); + byte signum = value >= 0 ? DECIMAL_PLUS : DECIMAL_MINUS; + int digitsNum = unscaledValueStr.Length; + + int len = (digitsNum >> 1) + 1; + byte[] payload = new byte[len]; + payload[len - 1] = signum; + int pos = 0; + char[] digits = unscaledValueStr.Substring(pos).ToCharArray(); + pos = digits.Length - 1; + for (int i = len - 1; i > 0; i--) + { + payload[i] |= (byte)(DigitFromChar(digits[pos--]) << 4); + payload[i - 1] |= (byte)DigitFromChar(digits[pos--]); + } + if (pos == 0) + { + payload[0] |= (byte)(DigitFromChar(digits[pos]) << 4); + } + + writer.Write(MsgPackExtDataTypes.Ext8); + writer.Write((byte)(len + 1)); + writer.Write(MP_DECIMAL); + writer.Write((byte)scale); + writer.Write(payload); + } + + private static (int, decimal) ExtractScaleFromDecimal(decimal val) + { + var bits = decimal.GetBits(val); + int scale = (bits[3] >> 16) & 0x7F; + decimal unscaledValue = new Decimal(bits[0], bits[1], bits[2], false, 0); + return (scale, unscaledValue); } private static int UnsignedRightShift(int signed, int places) @@ -144,7 +184,7 @@ private static int SecondNibbleFromByte(byte val) return val & 0x0F; } - private static char Digit(int val, int pos) + private static char CharFromDigit(int val, int pos) { var digit = (char)val; if (digit > 9) @@ -154,6 +194,11 @@ private static char Digit(int val, int pos) return digit; } + private static int DigitFromChar(char val) + { + return val - '0'; + } + private static decimal CreateDecimalFromDigits(char[] digits, int scale, bool isNegative) { int pos = 0; diff --git a/src/progaudi.tarantool/Converters/GuidConverter.cs b/src/progaudi.tarantool/Converters/GuidConverter.cs index 480de7e0..19b9dd0c 100644 --- a/src/progaudi.tarantool/Converters/GuidConverter.cs +++ b/src/progaudi.tarantool/Converters/GuidConverter.cs @@ -8,7 +8,7 @@ namespace ProGaudi.Tarantool.Client.Converters { /// - /// Converter for Tarantool uuid values, implemeted as MsgPack extension. + /// Converter for Tarantool uuid values, implemented as MsgPack extension. /// See https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type /// internal class GuidConverter : IMsgPackConverter @@ -43,7 +43,25 @@ public Guid Read(IMsgPackReader reader) public void Write(Guid value, IMsgPackWriter writer) { - throw new NotImplementedException(); + writer.Write(GuidDataType); + writer.Write(MP_UUID); + + var byteArray = value.ToByteArray(); + + // big-endian swap + SwapTwoBytes(byteArray, 0, 3); + SwapTwoBytes(byteArray, 1, 2); + SwapTwoBytes(byteArray, 4, 5); + SwapTwoBytes(byteArray, 6, 7); + + writer.Write(byteArray); + } + + private static void SwapTwoBytes(byte[] array, int index1, int index2) + { + var temp = array[index1]; + array[index1] = array[index2]; + array[index2] = temp; } } } diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs index 77a6e576..e3c32e8a 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs @@ -1,6 +1,7 @@ using Xunit; using Shouldly; using System.Text; +using System.ComponentModel; namespace progaudi.tarantool.integration.tests.DataTypes { @@ -104,17 +105,31 @@ public async Task DeserializeString_ShouldBeCorrectAsync() [InlineData("0.01")] [InlineData("0.001")] [InlineData("0.0001")] - public async Task DeserializeExtAsDecimal_ShouldBeCorrectAsync(string str) + + public async Task DeserializeExtAsDecimal_CorrectValue_ShouldBeDeserializedCorrectlyAsync(string str) { decimal n = Decimal.Parse(str); using var tarantoolClient = await GetTarantoolClient(); - var result = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{n}\")"); + var result = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")"); result.Data.ShouldBe(new[] { n }); var negativeResult = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{-n}\")"); negativeResult.Data.ShouldBe(new[] { -n }); } + [Theory] + [InlineData("0.12345678901234567890123456789")]// scale == 29 (max possible in .net is 28) + [InlineData("0.12345678901234567890123456789012345678")]// scale == 38 (max possible in .net is 28) + [InlineData("79228162514264337593543950336")]// max .net decimal + 1 + [InlineData("-79228162514264337593543950336")]// min .net decimal - 1 + public async Task DeserializeExtAsDecimal_IncorrectValue_OverflowExceptionThrown(string str) + { + using var tarantoolClient = await GetTarantoolClient(); + + await Assert.ThrowsAsync(async () => + await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")")); + } + [Fact] public async Task DeserializeBinary8_ShouldBeCorrectAsync() { diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs index e0244165..08e95a10 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs @@ -15,7 +15,7 @@ public class SerializationTests : TarantoolBaseTest [InlineData(false)] public async Task SerializeBoolean_ShouldBeCorrectAsync(bool val) { - await TestThatYouGetWhatYouGive(val); + await AssertThatYouGetWhatYouGive(val); } [Theory] @@ -24,17 +24,42 @@ public async Task SerializeBoolean_ShouldBeCorrectAsync(bool val) [InlineData(1000000)] public async Task SerializeInt_ShouldBeCorrectAsync(int val) { - await TestThatYouGetWhatYouGive(val); + await AssertThatYouGetWhatYouGive(val); } [Theory] [InlineData("test")] public async Task SerializeString_ShouldBeCorrectAsync(string val) { - await TestThatYouGetWhatYouGive(val); + await AssertThatYouGetWhatYouGive(val); } - private static async Task TestThatYouGetWhatYouGive(T val) + [Fact] + public async Task SerializeGuid_ShouldBeCorrectAsync() + { + await AssertThatYouGetWhatYouGive(Guid.NewGuid()); + } + + [Theory] + [InlineData("0")] + [InlineData("100500")] + [InlineData("0.1234567890123456789012345678")] + public async Task SerializeDecimal_ShouldBeCorrectAsync(string val) + { + decimal n = Decimal.Parse(val); + await AssertThatYouGetWhatYouGive(n); + await AssertThatYouGetWhatYouGive(-n); + } + + [Fact] + public async Task SerializeDatetime_ShouldBeCorrectAsync() + { + var dt = DateTime.UtcNow; + await AssertThatYouGetWhatYouGive(dt); + await AssertThatYouGetWhatYouGive((DateTimeOffset)dt); + } + + private static async Task AssertThatYouGetWhatYouGive(T val) { using var tarantoolClient = await GetTarantoolClient(); var result = await tarantoolClient.Eval, T>($"return ...", TarantoolTuple.Create(val)); From acf2b7684a848147d2235adbb21375709194a6de Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Wed, 28 Jun 2023 18:48:43 +0300 Subject: [PATCH 4/8] added missed reference --- src/progaudi.tarantool/progaudi.tarantool.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/progaudi.tarantool/progaudi.tarantool.csproj b/src/progaudi.tarantool/progaudi.tarantool.csproj index a568dcb2..e43e8d33 100644 --- a/src/progaudi.tarantool/progaudi.tarantool.csproj +++ b/src/progaudi.tarantool/progaudi.tarantool.csproj @@ -29,6 +29,7 @@ + From f0c561962c5391af75679c14c56685f7239fcb22 Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Wed, 28 Jun 2023 18:59:00 +0300 Subject: [PATCH 5/8] added serialization tests for tuple and dictionary --- .../DataTypes/SerializationTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs index 08e95a10..84aed8f7 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs @@ -59,6 +59,33 @@ public async Task SerializeDatetime_ShouldBeCorrectAsync() await AssertThatYouGetWhatYouGive((DateTimeOffset)dt); } + [Fact] + public async Task SerializeTuple_ShouldBeCorrectAsync() + { + await AssertThatYouGetWhatYouGive(Tuple.Create(1, true, "test", 1m)); + } + + [Fact] + public async Task SerializeDictionary_ShouldBeCorrectAsync() + { + using var tarantoolClient = await GetTarantoolClient(); + var expectedDict = new Dictionary() + { + { "foo", 1 }, + { "bar", 2 }, + { "baz", 3 } + }; + var result = await tarantoolClient.Eval>, Dictionary>($"return ...", TarantoolTuple.Create(expectedDict)); + + result.Data.Length.ShouldBe(1); + var actualDict = result.Data[0]; + foreach (var key in expectedDict.Keys) // order doesn't preserve, so we need to check key by key + { + actualDict.ContainsKey(key).ShouldBeTrue(); + actualDict[key].ShouldBe(expectedDict[key]); + } + } + private static async Task AssertThatYouGetWhatYouGive(T val) { using var tarantoolClient = await GetTarantoolClient(); From 25aa4c10fbcbf57b829968bf9c967eba177ee9a1 Mon Sep 17 00:00:00 2001 From: Paul Grigorenko Date: Thu, 6 Jul 2023 12:26:08 +0300 Subject: [PATCH 6/8] switch integration tests to NUnit, added new tests for Box --- .../DataTypes/DeserializationTests.cs | 128 +++++++++--------- .../DataTypes/SerializationTests.cs | 35 +++-- .../TarantoolBaseTest.cs | 5 + .../TarantoolBox/InsertTests.cs | 46 +++++++ .../TarantoolBox/SchemaTests.cs | 73 ++++++++++ ...rogaudi.tarantool.integration.tests.csproj | 9 +- 6 files changed, 205 insertions(+), 91 deletions(-) create mode 100644 src/tests/progaudi.tarantool.integration.tests/TarantoolBox/InsertTests.cs create mode 100644 src/tests/progaudi.tarantool.integration.tests/TarantoolBox/SchemaTests.cs diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs index e3c32e8a..094df503 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs @@ -1,7 +1,6 @@ -using Xunit; using Shouldly; using System.Text; -using System.ComponentModel; +using NUnit.Framework; namespace progaudi.tarantool.integration.tests.DataTypes { @@ -9,9 +8,10 @@ namespace progaudi.tarantool.integration.tests.DataTypes /// Test suite, where we create and return some values in Tarantool via Lua and Eval command, /// and check that this value deserialize into correspoding C# class/structure correctly /// + [TestFixture] public class DeserializationTests : TarantoolBaseTest { - [Fact] + [Test] public async Task DeserializeNull_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -20,7 +20,7 @@ public async Task DeserializeNull_ShouldBeCorrectAsync() result.Data[0].ShouldBeNull(); } - [Fact] + [Test] public async Task DeserializeNil_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -29,9 +29,8 @@ public async Task DeserializeNil_ShouldBeCorrectAsync() result.Data[0].ShouldBeNull(); } - [Theory] - [InlineData(true)] - [InlineData(false)] + [TestCase(true)] + [TestCase(false)] public async Task DeserializeBoolean_ShouldBeCorrectAsync(bool val) { using var tarantoolClient = await GetTarantoolClient(); @@ -40,10 +39,9 @@ public async Task DeserializeBoolean_ShouldBeCorrectAsync(bool val) result.Data[0].ShouldBe(val); } - [Theory] - [InlineData(1)] - [InlineData(0)] - [InlineData(-1)] + [TestCase(1)] + [TestCase(0)] + [TestCase(-1)] public async Task DeserializeInt_ShouldBeCorrectAsync(int val) { using var tarantoolClient = await GetTarantoolClient(); @@ -51,7 +49,7 @@ public async Task DeserializeInt_ShouldBeCorrectAsync(int val) result.Data.ShouldBe(new[] { val }); } - [Fact] + [Test] public async Task DeserializeFloat64_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -60,7 +58,7 @@ public async Task DeserializeFloat64_ShouldBeCorrectAsync() Math.Abs(result.Data[0] - Math.Sqrt(2)).ShouldBeLessThan(double.Epsilon); } - [Fact] + [Test] public async Task DeserializeString_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -68,43 +66,42 @@ public async Task DeserializeString_ShouldBeCorrectAsync() var result = await tarantoolClient.Eval($"return '{expectedStr}'"); result.Data.ShouldBe(new[] { expectedStr }); } - - [Theory] - [InlineData("2.7182818284590452353602874714")] - [InlineData("2.718281828459045235360287471")] - [InlineData("2.71828182845904523536028747")] - [InlineData("2.7182818284590452353602874")] - [InlineData("2.718281828459045235360287")] - [InlineData("2.71828182845904523536028")] - [InlineData("2.7182818284590452353602")] - [InlineData("2.718281828459045235360")] - [InlineData("2.71828182845904523536")] - [InlineData("2.7182818284590452353")] - [InlineData("2.718281828459045235")] - [InlineData("2.71828182845904523")] - [InlineData("2.7182818284590452")] - [InlineData("2.718281828459045")] - [InlineData("2.71828182845904")] - [InlineData("2.7182818284590")] - [InlineData("2.718281828459")] - [InlineData("2.71828182845")] - [InlineData("2.7182818284")] - [InlineData("2.718281828")] - [InlineData("2.71828182")] - [InlineData("2.7182818")] - [InlineData("2.718281")] - [InlineData("2.71828")] - [InlineData("2.7182")] - [InlineData("2.718")] - [InlineData("2.71")] - [InlineData("2.7")] - [InlineData("2")] - [InlineData("0")] - [InlineData("100000")] - [InlineData("0.1")] - [InlineData("0.01")] - [InlineData("0.001")] - [InlineData("0.0001")] + + [TestCase("2.7182818284590452353602874714")] + [TestCase("2.718281828459045235360287471")] + [TestCase("2.71828182845904523536028747")] + [TestCase("2.7182818284590452353602874")] + [TestCase("2.718281828459045235360287")] + [TestCase("2.71828182845904523536028")] + [TestCase("2.7182818284590452353602")] + [TestCase("2.718281828459045235360")] + [TestCase("2.71828182845904523536")] + [TestCase("2.7182818284590452353")] + [TestCase("2.718281828459045235")] + [TestCase("2.71828182845904523")] + [TestCase("2.7182818284590452")] + [TestCase("2.718281828459045")] + [TestCase("2.71828182845904")] + [TestCase("2.7182818284590")] + [TestCase("2.718281828459")] + [TestCase("2.71828182845")] + [TestCase("2.7182818284")] + [TestCase("2.718281828")] + [TestCase("2.71828182")] + [TestCase("2.7182818")] + [TestCase("2.718281")] + [TestCase("2.71828")] + [TestCase("2.7182")] + [TestCase("2.718")] + [TestCase("2.71")] + [TestCase("2.7")] + [TestCase("2")] + [TestCase("0")] + [TestCase("100000")] + [TestCase("0.1")] + [TestCase("0.01")] + [TestCase("0.001")] + [TestCase("0.0001")] public async Task DeserializeExtAsDecimal_CorrectValue_ShouldBeDeserializedCorrectlyAsync(string str) { @@ -117,20 +114,19 @@ public async Task DeserializeExtAsDecimal_CorrectValue_ShouldBeDeserializedCorre negativeResult.Data.ShouldBe(new[] { -n }); } - [Theory] - [InlineData("0.12345678901234567890123456789")]// scale == 29 (max possible in .net is 28) - [InlineData("0.12345678901234567890123456789012345678")]// scale == 38 (max possible in .net is 28) - [InlineData("79228162514264337593543950336")]// max .net decimal + 1 - [InlineData("-79228162514264337593543950336")]// min .net decimal - 1 + [TestCase("0.12345678901234567890123456789")]// scale == 29 (max possible in .net is 28) + [TestCase("0.12345678901234567890123456789012345678")]// scale == 38 (max possible in .net is 28) + [TestCase("79228162514264337593543950336")]// max .net decimal + 1 + [TestCase("-79228162514264337593543950336")]// min .net decimal - 1 public async Task DeserializeExtAsDecimal_IncorrectValue_OverflowExceptionThrown(string str) { using var tarantoolClient = await GetTarantoolClient(); - await Assert.ThrowsAsync(async () => + Assert.ThrowsAsync(async () => await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")")); } - [Fact] + [Test] public async Task DeserializeBinary8_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -139,7 +135,7 @@ public async Task DeserializeBinary8_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { expectedByteArray }); } - [Fact] + [Test] public async Task DeserializeBinary16_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -149,7 +145,7 @@ public async Task DeserializeBinary16_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { expectedByteArray }); } - [Fact] + [Test] public async Task DeserializeExtAsGuid_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -158,7 +154,7 @@ public async Task DeserializeExtAsGuid_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { guid }); } - [Fact] + [Test] public async Task DeserializeExtAsDatetime_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -185,7 +181,7 @@ public async Task DeserializeExtAsDatetime_ShouldBeCorrectAsync() actualDt.Millisecond.ShouldBe(dt.Millisecond); } - [Fact] + [Test] public async Task DeserializeIntArray_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -194,7 +190,7 @@ public async Task DeserializeIntArray_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { arr }); } - [Fact] + [Test] public async Task DeserializeBooleanArray_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -203,7 +199,7 @@ public async Task DeserializeBooleanArray_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { arr }); } - [Fact] + [Test] public async Task DeserializeFloat64Array_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -216,7 +212,7 @@ public async Task DeserializeFloat64Array_ShouldBeCorrectAsync() Math.Abs(result.Data[0][2] - Math.Sqrt(5)).ShouldBeLessThan(double.Epsilon); } - [Fact] + [Test] public async Task DeserializeStringArray_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -225,7 +221,7 @@ public async Task DeserializeStringArray_ShouldBeCorrectAsync() result.Data.ShouldBe(new[] { arr }); } - [Fact] + [Test] public async Task DeserializeMixedArrayToTuple_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); @@ -236,7 +232,7 @@ public async Task DeserializeMixedArrayToTuple_ShouldBeCorrectAsync() result.Data[0].Item3.ShouldBe("foo"); } - [Fact] + [Test] public async Task DeserializeMapToDictionary_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs index 84aed8f7..3e895fa3 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs @@ -1,6 +1,6 @@ using ProGaudi.Tarantool.Client.Model; using Shouldly; -using Xunit; +using NUnit.Framework; namespace progaudi.tarantool.integration.tests.DataTypes { @@ -8,42 +8,39 @@ namespace progaudi.tarantool.integration.tests.DataTypes /// Test suite, where we check correct data types serialization, when we pass values into Eval command, /// and also check that this value deserialize into correspoding C# class/structure correctly /// + [TestFixture] public class SerializationTests : TarantoolBaseTest { - [Theory] - [InlineData(true)] - [InlineData(false)] + [TestCase(true)] + [TestCase(false)] public async Task SerializeBoolean_ShouldBeCorrectAsync(bool val) { await AssertThatYouGetWhatYouGive(val); } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - [InlineData(1000000)] + + [TestCase(0)] + [TestCase(-1)] + [TestCase(1000000)] public async Task SerializeInt_ShouldBeCorrectAsync(int val) { await AssertThatYouGetWhatYouGive(val); } - [Theory] - [InlineData("test")] + [TestCase("test")] public async Task SerializeString_ShouldBeCorrectAsync(string val) { await AssertThatYouGetWhatYouGive(val); } - [Fact] + [Test] public async Task SerializeGuid_ShouldBeCorrectAsync() { await AssertThatYouGetWhatYouGive(Guid.NewGuid()); } - [Theory] - [InlineData("0")] - [InlineData("100500")] - [InlineData("0.1234567890123456789012345678")] + [TestCase("0")] + [TestCase("100500")] + [TestCase("0.1234567890123456789012345678")] public async Task SerializeDecimal_ShouldBeCorrectAsync(string val) { decimal n = Decimal.Parse(val); @@ -51,7 +48,7 @@ public async Task SerializeDecimal_ShouldBeCorrectAsync(string val) await AssertThatYouGetWhatYouGive(-n); } - [Fact] + [Test] public async Task SerializeDatetime_ShouldBeCorrectAsync() { var dt = DateTime.UtcNow; @@ -59,13 +56,13 @@ public async Task SerializeDatetime_ShouldBeCorrectAsync() await AssertThatYouGetWhatYouGive((DateTimeOffset)dt); } - [Fact] + [Test] public async Task SerializeTuple_ShouldBeCorrectAsync() { await AssertThatYouGetWhatYouGive(Tuple.Create(1, true, "test", 1m)); } - [Fact] + [Test] public async Task SerializeDictionary_ShouldBeCorrectAsync() { using var tarantoolClient = await GetTarantoolClient(); diff --git a/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs b/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs index fe0daa66..98c3cdcf 100644 --- a/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs +++ b/src/tests/progaudi.tarantool.integration.tests/TarantoolBaseTest.cs @@ -11,6 +11,11 @@ public static async Task GetTarantoolClient(string userName = null, string return await Box.Connect(BuildConnectionString(userName, password)); } + public static string RandomSpaceName() + { + return "sp_" + Guid.NewGuid().ToString().Replace("-", ""); + } + private static string BuildConnectionString(string userName, string password) { var userToken = (userName, password) diff --git a/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/InsertTests.cs b/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/InsertTests.cs new file mode 100644 index 00000000..07fa4822 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/InsertTests.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using ProGaudi.Tarantool.Client.Model; +using Shouldly; + +namespace progaudi.tarantool.integration.tests.TarantoolBox +{ + [TestFixture] + internal class InsertTests : TarantoolBaseTest + { + static readonly string SpaceName = RandomSpaceName(); + const string IndexName = "primary_idx"; + + [SetUp] + public async Task Setup() + { + using var tarantoolClient = await GetTarantoolClient(); + await tarantoolClient.Eval($"local mysp = box.schema.space.create('{SpaceName}'); " + + $"mysp:format({{{{'id',type = 'scalar'}}, {{'a1',type = 'scalar', is_nullable = true}}, {{'a2',type = 'string', is_nullable = true}}, {{'a3',type = 'string', is_nullable = true}}}}); " + + $"mysp:create_index('{IndexName}', {{parts = {{'id'}}}}); " + + $"return 1"); + } + + [TearDown] + public async Task TearDown() + { + using var tarantoolClient = await GetTarantoolClient(); + await tarantoolClient.Eval($"box.space.{SpaceName}:drop(); return 1"); + } + + [TestCase(777)] + public async Task InsertIntToSpaceAndGetItBack_ShouldBeCorrectAsync(int val) + { + using var tarantoolClient = await GetTarantoolClient(); + var schema = tarantoolClient.GetSchema(); + var space = schema[SpaceName]; + space.Name.ShouldBe(SpaceName); + var id = 1; + + await space.Insert(TarantoolTuple.Create(id, val)); + var tuple = await space.Get, ValueTuple>(ValueTuple.Create(id)); + + tuple.Item1.ShouldBe(id); + tuple.Item2.ShouldBe(val); + } + } +} diff --git a/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/SchemaTests.cs b/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/SchemaTests.cs new file mode 100644 index 00000000..c3ff58d3 --- /dev/null +++ b/src/tests/progaudi.tarantool.integration.tests/TarantoolBox/SchemaTests.cs @@ -0,0 +1,73 @@ +using NUnit.Framework; +using Shouldly; + +namespace progaudi.tarantool.integration.tests.TarantoolBox +{ + [TestFixture] + public class SchemaTests : TarantoolBaseTest + { + const string SpaceName = "schema_test_space"; + const string IndexName = "primary_idx"; + + [SetUp] + public async Task Setup() + { + using var tarantoolClient = await GetTarantoolClient(); + await tarantoolClient.Eval($"local mysp = box.schema.space.create('{SpaceName}'); " + + $"mysp:format({{{{'a',type = 'number'}}}}); " + + $"mysp:create_index('{IndexName}', {{parts = {{1, 'number'}}}}); " + + $"return 1"); + } + + [TearDown] + public async Task TearDown() + { + using var tarantoolClient = await GetTarantoolClient(); + await tarantoolClient.Eval($"box.space.{SpaceName}:drop(); return 1"); + } + + [Test] + public async Task GetNotExistingSpace_ShouldThrowArgumentException() + { + using var tarantoolClient = await GetTarantoolClient(); + var schema = tarantoolClient.GetSchema(); + + Should.Throw(() => + { + var _ = schema["not_existing_space"]; + }); + } + + [Test] + public async Task GetExistingSpace_ShouldReturnCorrectly() + { + using var tarantoolClient = await GetTarantoolClient(); + var schema = tarantoolClient.GetSchema(); + var space = schema[SpaceName]; + space.Name.ShouldBe(SpaceName); + } + + [Test] + public async Task GetNotExistingIndex_ShouldThrowArgumentException() + { + using var tarantoolClient = await GetTarantoolClient(); + var schema = tarantoolClient.GetSchema(); + var space = schema[SpaceName]; + + Should.Throw(() => + { + var _ = schema["not_existing_index"]; + }); + } + + [Test] + public async Task GetExistingIndex_ShouldReturnCorrectly() + { + using var tarantoolClient = await GetTarantoolClient(); + var schema = tarantoolClient.GetSchema(); + var space = schema[SpaceName]; + var index = space[IndexName]; + index.Name.ShouldBe(IndexName); + } + } +} diff --git a/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj b/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj index c0f2cdb6..373d4922 100644 --- a/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj +++ b/src/tests/progaudi.tarantool.integration.tests/progaudi.tarantool.integration.tests.csproj @@ -10,15 +10,12 @@ - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + From 8bc27551e10bc6cc521de5a80e7a16cb05872ea2 Mon Sep 17 00:00:00 2001 From: "MAIL\\p.grigorenko" Date: Thu, 6 Jul 2023 12:55:49 +0300 Subject: [PATCH 7/8] fix decimal serialization (use invariant culture decimal separators) --- src/progaudi.tarantool/Converters/DecimalConverter.cs | 3 ++- .../DataTypes/DeserializationTests.cs | 5 +++-- .../DataTypes/SerializationTests.cs | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/progaudi.tarantool/Converters/DecimalConverter.cs b/src/progaudi.tarantool/Converters/DecimalConverter.cs index 399d8cc2..ad08ff72 100644 --- a/src/progaudi.tarantool/Converters/DecimalConverter.cs +++ b/src/progaudi.tarantool/Converters/DecimalConverter.cs @@ -2,6 +2,7 @@ using ProGaudi.Tarantool.Client.Model.Enums; using ProGaudi.Tarantool.Client.Utils; using System; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -232,7 +233,7 @@ private static decimal CreateDecimalFromDigits(char[] digits, int scale, bool is { sb.Insert(0, '-'); } - return Decimal.Parse(sb.ToString()); + return Decimal.Parse(sb.ToString(), CultureInfo.InvariantCulture); } } } diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs index 094df503..ea313de8 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/DeserializationTests.cs @@ -1,6 +1,7 @@ using Shouldly; using System.Text; using NUnit.Framework; +using System.Globalization; namespace progaudi.tarantool.integration.tests.DataTypes { @@ -105,12 +106,12 @@ public async Task DeserializeString_ShouldBeCorrectAsync() public async Task DeserializeExtAsDecimal_CorrectValue_ShouldBeDeserializedCorrectlyAsync(string str) { - decimal n = Decimal.Parse(str); + decimal n = Decimal.Parse(str, CultureInfo.InvariantCulture); using var tarantoolClient = await GetTarantoolClient(); var result = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")"); result.Data.ShouldBe(new[] { n }); - var negativeResult = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{-n}\")"); + var negativeResult = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"-{str}\")"); negativeResult.Data.ShouldBe(new[] { -n }); } diff --git a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs index 3e895fa3..7b9e6242 100644 --- a/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs +++ b/src/tests/progaudi.tarantool.integration.tests/DataTypes/SerializationTests.cs @@ -1,6 +1,7 @@ using ProGaudi.Tarantool.Client.Model; using Shouldly; using NUnit.Framework; +using System.Globalization; namespace progaudi.tarantool.integration.tests.DataTypes { @@ -43,7 +44,7 @@ public async Task SerializeGuid_ShouldBeCorrectAsync() [TestCase("0.1234567890123456789012345678")] public async Task SerializeDecimal_ShouldBeCorrectAsync(string val) { - decimal n = Decimal.Parse(val); + decimal n = Decimal.Parse(val, CultureInfo.InvariantCulture); await AssertThatYouGetWhatYouGive(n); await AssertThatYouGetWhatYouGive(-n); } From daf68dee6c6ea97d775ae075d41f8d41698dd31f Mon Sep 17 00:00:00 2001 From: "MAIL\\p.grigorenko" Date: Wed, 12 Jul 2023 12:30:38 +0300 Subject: [PATCH 8/8] add integration tests project to sln --- progaudi.tarantool.sln | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/progaudi.tarantool.sln b/progaudi.tarantool.sln index c0ca998f..90d3a478 100644 --- a/progaudi.tarantool.sln +++ b/progaudi.tarantool.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "progaudi.tarantool", "src\progaudi.tarantool\progaudi.tarantool.csproj", "{DD007E9F-FB2D-4351-AAB7-F2D367B295C4}" EndProject @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "progaudi.tarantool.tests", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{14BAEDF1-BEFC-4FB2-AAC9-08D397191216}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "progaudi.tarantool.benchmark", "src\progaudi.tarantool.benchmark\progaudi.tarantool.benchmark.csproj", "{CD96AC2D-505F-4FDF-82FB-76F51CC28FF4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "progaudi.tarantool.benchmark", "src\progaudi.tarantool.benchmark\progaudi.tarantool.benchmark.csproj", "{CD96AC2D-505F-4FDF-82FB-76F51CC28FF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "progaudi.tarantool.integration.tests", "src\tests\progaudi.tarantool.integration.tests\progaudi.tarantool.integration.tests.csproj", "{B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -57,6 +59,18 @@ Global {CD96AC2D-505F-4FDF-82FB-76F51CC28FF4}.Release|x64.Build.0 = Release|Any CPU {CD96AC2D-505F-4FDF-82FB-76F51CC28FF4}.Release|x86.ActiveCfg = Release|Any CPU {CD96AC2D-505F-4FDF-82FB-76F51CC28FF4}.Release|x86.Build.0 = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|x64.Build.0 = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Debug|x86.Build.0 = Debug|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|Any CPU.Build.0 = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|x64.ActiveCfg = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|x64.Build.0 = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|x86.ActiveCfg = Release|Any CPU + {B26A117F-6E7B-4AB0-B3D4-DDBCFC8732C8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE