// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using System; using System.Collections.Generic; using System.Data.SqlTypes; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Text.RegularExpressions; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Moq; using NUnit.Framework; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage { public class ServiceBufferReaderWriterTests { [Test] public void ReaderStreamNull() { // If: I create a service buffer file stream reader with a null stream // Then: It should throw an exception Assert.Throws(() => new ServiceBufferFileStreamReader(null, new QueryExecutionSettings())); } [Test] public void ReaderSettingsNull() { // If: I create a service buffer file stream reader with null settings // Then: It should throw an exception Assert.Throws(() => new ServiceBufferFileStreamReader(Stream.Null, null)); } [Test] public void ReaderInvalidStreamCannotRead() { // If: I create a service buffer file stream reader with a stream that cannot be read // Then: I should get an exception var invalidStream = new Mock(); invalidStream.SetupGet(s => s.CanRead).Returns(false); invalidStream.SetupGet(s => s.CanSeek).Returns(true); Assert.Throws(() => { ServiceBufferFileStreamReader obj = new ServiceBufferFileStreamReader(invalidStream.Object, new QueryExecutionSettings()); obj.Dispose(); }); } [Test] public void ReaderInvalidStreamCannotSeek() { // If: I create a service buffer file stream reader with a stream that cannot seek // Then: I should get an exception var invalidStream = new Mock(); invalidStream.SetupGet(s => s.CanRead).Returns(true); invalidStream.SetupGet(s => s.CanSeek).Returns(false); Assert.Throws(() => { ServiceBufferFileStreamReader obj = new ServiceBufferFileStreamReader(invalidStream.Object, new QueryExecutionSettings()); obj.Dispose(); }); } [Test] public void WriterStreamNull() { // If: I create a service buffer file stream writer with a null stream // Then: It should throw an exception Assert.Throws(() => new ServiceBufferFileStreamWriter(null, new QueryExecutionSettings())); } [Test] public void WriterSettingsNull() { // If: I create a service buffer file stream writer with null settings // Then: It should throw an exception Assert.Throws(() => new ServiceBufferFileStreamWriter(Stream.Null, null)); } [Test] public void WriterInvalidStreamCannotWrite() { // If: I create a service buffer file stream writer with a stream that cannot be read // Then: I should get an exception var invalidStream = new Mock(); invalidStream.SetupGet(s => s.CanWrite).Returns(false); invalidStream.SetupGet(s => s.CanSeek).Returns(true); Assert.Throws(() => { ServiceBufferFileStreamWriter obj = new ServiceBufferFileStreamWriter(invalidStream.Object, new QueryExecutionSettings()); obj.Dispose(); }); } [Test] public void WriterInvalidStreamCannotSeek() { // If: I create a service buffer file stream writer with a stream that cannot seek // Then: I should get an exception var invalidStream = new Mock(); invalidStream.SetupGet(s => s.CanWrite).Returns(true); invalidStream.SetupGet(s => s.CanSeek).Returns(false); Assert.Throws(() => { ServiceBufferFileStreamWriter obj = new ServiceBufferFileStreamWriter(invalidStream.Object, new QueryExecutionSettings()); obj.Dispose(); }); } [SuppressMessage("ReSharper", "UnusedParameter.Local")] private static string VerifyReadWrite(int valueLength, T value, Func writeFunc, Func readFunc, QueryExecutionSettings overrideSettings = null) { // Setup: Create a mock file stream byte[] storage = new byte[8192]; overrideSettings = overrideSettings ?? new QueryExecutionSettings(); const long rowId = 100; // If: // ... I write a type T to the writer using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(new MemoryStream(storage), overrideSettings)) { int writtenBytes = writeFunc(writer, value); Assert.AreEqual(valueLength, writtenBytes); } // ... And read the type T back FileStreamReadResult outValue; using (ServiceBufferFileStreamReader reader = new ServiceBufferFileStreamReader(new MemoryStream(storage), overrideSettings)) { outValue = readFunc(reader, rowId); } // Then: Assert.AreEqual(value, outValue.Value.RawObject); Assert.AreEqual(valueLength, outValue.TotalLength); Assert.NotNull(outValue.Value); // ... The id we set should be stored in the returned db cell value Assert.AreEqual(rowId, outValue.Value.RowId); return outValue.Value.DisplayValue; } [Test] public void Int16([Values( 0, 10, -10, short.MaxValue, // Two byte number short.MinValue // Negative two byte number )] short value) { VerifyReadWrite(sizeof(short) + 1, value, (writer, val) => writer.WriteInt16(val), (reader, rowId) => reader.ReadInt16(0, rowId)); } [Test] public void Int32([Values( 0, 10, -10, short.MaxValue, // Two byte number short.MinValue, // Negative two byte number int.MaxValue, // Four byte number int.MinValue // Negative four byte number )] int value) { VerifyReadWrite(sizeof(int) + 1, value, (writer, val) => writer.WriteInt32(val), (reader, rowId) => reader.ReadInt32(0, rowId)); } [Test] public void Int64([Values( 0, 10, -10, short.MaxValue, // Two byte number short.MinValue, // Negative two byte number int.MaxValue, // Four byte number int.MinValue, // Negative four byte number long.MaxValue, // Eight byte number long.MinValue // Negative eight byte number )] long value) { VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteInt64(val), (reader, rowId) => reader.ReadInt64(0, rowId)); } [Test] public void Byte([Values(0,10)] byte value) { VerifyReadWrite(sizeof(byte) + 1, value, (writer, val) => writer.WriteByte(val), (reader, rowId) => reader.ReadByte(0, rowId)); } [Test] public void Char([Values('a', '1', (char)0x9152)] // Test something in the UTF-16 space char value) { VerifyReadWrite(sizeof(char) + 1, value, (writer, val) => writer.WriteChar(val), (reader, rowId) => reader.ReadChar(0, rowId)); } [Test] public void Boolean([Values] bool value, [Values] bool preferNumeric) { string displayValue = VerifyReadWrite(sizeof(bool) + 1, value, (writer, val) => writer.WriteBoolean(val), (reader, rowId) => reader.ReadBoolean(0, rowId), new QueryExecutionSettings {DisplayBitAsNumber = preferNumeric} ); // Validate the display value if (preferNumeric) { int output; Assert.True(int.TryParse(displayValue, out output)); } else { bool output; Assert.True(bool.TryParse(displayValue, out output)); } } [Test] public void Single([Values( 0, 10.1F, -10.1F, float.MinValue, float.MaxValue, float.PositiveInfinity, float.NegativeInfinity )] float value) { VerifyReadWrite(sizeof(float) + 1, value, (writer, val) => writer.WriteSingle(val), (reader, rowId) => reader.ReadSingle(0, rowId)); } [Test] public void Double([Values( 0, 10.1, -10.1, float.MinValue, float.MaxValue, float.PositiveInfinity, float.NegativeInfinity, double.PositiveInfinity, double.NegativeInfinity, double.MinValue, double.MaxValue )]double value) { VerifyReadWrite(sizeof(double) + 1, value, (writer, val) => writer.WriteDouble(val), (reader, rowId) => reader.ReadDouble(0, rowId)); } [Test] public void SqlDecimalTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because SqlDecimal values can't be written as constant expressions SqlDecimal[] testValues = { SqlDecimal.MaxValue, SqlDecimal.MinValue, new SqlDecimal(0x01, 0x01, true, 0, 0, 0, 0) }; foreach (SqlDecimal value in testValues) { int valueLength = 4 + value.BinData.Length; VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteSqlDecimal(val), (reader, rowId) => reader.ReadSqlDecimal(0, rowId)); } } [Test] public void Decimal() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because Decimal values can't be written as constant expressions decimal[] testValues = { decimal.Zero, decimal.One, decimal.MinusOne, decimal.MinValue, decimal.MaxValue }; foreach (decimal value in testValues) { int valueLength = decimal.GetBits(value).Length*4 + 1; VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteDecimal(val), (reader, rowId) => reader.ReadDecimal(0, rowId)); } } [Test] public void DateTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; // Setup: Create a DATE column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn {DataTypeName = "DaTe"}); foreach (DateTime value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), (reader, rowId) => reader.ReadDateTime(0, rowId, col)); // Make sure the display value does not have a time string Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2}$")); } } [Test] public void DateTimeTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; // Setup: Create a DATETIME column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn {DataTypeName = "DaTeTiMe"}); foreach (DateTime value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), (reader, rowId) => reader.ReadDateTime(0, rowId, col)); // Make sure the display value has a time string with 3 milliseconds Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}\.[\d]{3}$")); } } [Test] public void DateTime2Test([Values(1,2,3,4,5,6,7)] int precision) { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; // Setup: Create a DATETIME column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn { DataTypeName = "DaTeTiMe2", NumericScale = precision }); foreach (DateTime value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), (reader, rowId) => reader.ReadDateTime(0, rowId, col)); // Make sure the display value has a time string with variable number of milliseconds Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}")); if (precision > 0) { Assert.True(Regex.IsMatch(displayValue, $@"\.[\d]{{{precision}}}$")); } } } [Test] public void DateTime2ZeroScaleTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; // Setup: Create a DATETIME2 column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn {DataTypeName = "DaTeTiMe2", NumericScale = 0}); foreach (DateTime value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), (reader, rowId) => reader.ReadDateTime(0, rowId, col)); // Make sure the display value has a time string with 0 milliseconds Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}$")); } } [Test] public void DateTime2InvalidScaleTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; // Setup: Create a DATETIME2 column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn {DataTypeName = "DaTeTiMe2", NumericScale = 255}); foreach (DateTime value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), (reader, rowId) => reader.ReadDateTime(0, rowId, col)); // Make sure the display value has a time string with 7 milliseconds Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}\.[\d]{7}$")); } } [Test] public void DateTimeOffsetTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because DateTimeOffset values can't be written as constant expressions DateTimeOffset[] testValues = { DateTimeOffset.Now, DateTimeOffset.UtcNow, DateTimeOffset.MinValue, DateTimeOffset.MaxValue }; // Setup: Create a DATETIMEOFFSET column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn { DataTypeName = "datetimeoffset", NumericScale = 6 }); foreach (DateTimeOffset value in testValues) { string displayValue = VerifyReadWrite(sizeof(long)*2 + 1, value, (writer, val) => writer.WriteDateTimeOffset(val), (reader, rowId) => reader.ReadDateTimeOffset(0, rowId, col)); // Make sure the display value has a time string with 6 milliseconds and a time zone Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}\.[\d]{6} [+-][01][\d]:[\d]{2}$")); } } [Test] public void DateTimeOffsetZeroScaleTest() { // Setup: Create some test values DateTimeOffset[] testValues = { DateTimeOffset.Now, DateTimeOffset.UtcNow, DateTimeOffset.MinValue, DateTimeOffset.MaxValue }; // Setup: Create a DATETIMEOFFSET column DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn { DataTypeName = "datetimeoffset", NumericScale = 0 }); foreach (DateTimeOffset value in testValues) { string displayValue = VerifyReadWrite(sizeof(long) * 2 + 1, value, (writer, val) => writer.WriteDateTimeOffset(val), (reader, rowId) => reader.ReadDateTimeOffset(0, rowId, col)); // Make sure the display value has a time string with no millisecond and a time zone Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2} [+-][01][\d]:[\d]{2}$")); } } [Test] public void TimeSpanTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because TimeSpan values can't be written as constant expressions TimeSpan[] testValues = { TimeSpan.Zero, TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.FromMinutes(60) }; foreach (TimeSpan value in testValues) { VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteTimeSpan(val), (reader, rowId) => reader.ReadTimeSpan(0, rowId)); } } [Test] public void StringNullTest() { // Setup: Create a mock file stream using (MemoryStream stream = new MemoryStream(new byte[8192])) { // If: // ... I write null as a string to the writer using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(stream, new QueryExecutionSettings())) { // Then: // ... I should get an argument null exception Assert.Throws(() => writer.WriteString(null)); } } } [Test, Sequential] public void StringTest([Values(0,1,1,100,512)] int length, [Values(null, new[] { 'j' }, new[] { (char)0x9152 }, new[] { 'j', (char)0x9152 }, // Test alternating utf-16/ascii characters new[] { 'j', (char)0x9152 })] // Test that requires a 4 byte length char[] values) { // Setup: // ... Generate the test value StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { sb.Append(values[i%values.Length]); } string value = sb.ToString(); int lengthLength = length == 0 || length > 255 ? 5 : 1; VerifyReadWrite(sizeof(char)*length + lengthLength, value, (writer, val) => writer.WriteString(value), (reader, rowId) => reader.ReadString(0, rowId)); } [Test] public void BytesNullTest() { // Setup: Create a mock file stream wrapper using (MemoryStream stream = new MemoryStream(new byte[8192])) { // If: // ... I write null as a string to the writer using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(stream, new QueryExecutionSettings())) { // Then: // ... I should get an argument null exception Assert.Throws(() => writer.WriteBytes(null)); } } } [Test, Sequential] public void Bytes([Values(0, 1, 1, 100, 512)] int length, [Values(new byte[] { 0x00 }, // Test of empty byte[] new byte[] { 0x00 }, new byte[] { 0xFF }, new byte[] { 0x10, 0xFF, 0x00 }, new byte[] { 0x10, 0xFF, 0x00 } // Test that requires a 4 byte length )] byte[] values) { // Setup: // ... Generate the test value List sb = new List(); for (int i = 0; i < length; i++) { sb.Add(values[i % values.Length]); } byte[] value = sb.ToArray(); int lengthLength = length == 0 || length > 255 ? 5 : 1; int valueLength = sizeof(byte)*length + lengthLength; VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteBytes(value), (reader, rowId) => reader.ReadBytes(0, rowId)); } private static IEnumerable GuidTestParameters { get { yield return Guid.Empty; yield return Guid.NewGuid(); yield return Guid.NewGuid(); } } [Test] public void GuidTest() { foreach (var testValue in GuidTestParameters) { VerifyReadWrite(testValue.ToByteArray().Length + 1, testValue, (writer, val) => writer.WriteGuid(testValue), (reader, rowId) => reader.ReadGuid(0, rowId)); } } [Test] public void MoneyTest() { // Setup: Create some test values // NOTE: We are doing these here instead of InlineData because SqlMoney can't be written as a constant expression SqlMoney[] monies = { SqlMoney.Zero, SqlMoney.MinValue, SqlMoney.MaxValue, new SqlMoney(1.02) }; foreach (SqlMoney money in monies) { VerifyReadWrite(sizeof(decimal) + 1, money, (writer, val) => writer.WriteMoney(money), (reader, rowId) => reader.ReadMoney(0, rowId)); } } } }