Feature: Writing Execute Results to Temp File (#35)

* WIP for buffering in temporary file

* Adding support for writing to disk for buffering

* WIP - Adding file reader, factory for reader/writer

* Making long list use generics and implement IEnumerable

* Reading/Writing from file is working

* Removing unused 'skipValue' logic

* More tweaks to file buffer

Adding logic for cleaning up the temp files
Adding fix for empty/null column names

* Adding comments and cleanup

* Unit tests for FileStreamWrapper

* WIP adding more unit tests, and finishing up wiring up the output writers

* Finishing up initial unit tests

* Fixing bugs with long fields

* Squashed commit of the following:

commit df0ffc12a46cb286d801d08689964eac08ad71dd
Author: Benjamin Russell <beruss@microsoft.com>
Date:   Wed Sep 7 14:45:39 2016 -0700

    Removing last bit of async for file writing.

    We're seeing a 8x improvement of file write speeds!

commit 08a4b9f32e825512ca24d5dc03ef5acbf7cc6d94
Author: Benjamin Russell <beruss@microsoft.com>
Date:   Wed Sep 7 11:23:06 2016 -0700

    Removing async wrappers

* Rolling back test code for Program.cs

* Changes as per code review

* Fixing broken unit tests

* More fixes for codereview
This commit is contained in:
Benjamin Russell
2016-09-08 17:55:11 -07:00
committed by GitHub
parent 903eab61d1
commit 8aa3d524fc
24 changed files with 4050 additions and 195 deletions

View File

@@ -0,0 +1,221 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.DataStorage
{
public class FileStreamWrapperTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void InitInvalidFilenameParameter(string fileName)
{
// If:
// ... I have a file stream wrapper that is initialized with invalid fileName
// Then:
// ... It should throw an argument null exception
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
Assert.Throws<ArgumentNullException>(() => fsw.Init(fileName, 8192, FileAccess.Read));
}
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void InitInvalidBufferLength(int bufferLength)
{
// If:
// ... I have a file stream wrapper that is initialized with an invalid buffer length
// Then:
// ... I should throw an argument out of range exception
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
Assert.Throws<ArgumentOutOfRangeException>(() => fsw.Init("validFileName", bufferLength, FileAccess.Read));
}
}
[Fact]
public void InitInvalidFileAccessMode()
{
// If:
// ... I attempt to open a file stream wrapper that is initialized with an invalid file
// access mode
// Then:
// ... I should get an invalid argument exception
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
Assert.Throws<ArgumentException>(() => fsw.Init("validFileName", 8192, FileAccess.Write));
}
}
[Fact]
public void InitSuccessful()
{
string fileName = Path.GetTempFileName();
try
{
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
// If:
// ... I have a file stream wrapper that is initialized with valid parameters
fsw.Init(fileName, 8192, FileAccess.ReadWrite);
// Then:
// ... The file should exist
FileInfo fileInfo = new FileInfo(fileName);
Assert.True(fileInfo.Exists);
// ... The file should be marked as hidden
Assert.True((fileInfo.Attributes & FileAttributes.Hidden) != 0);
}
}
finally
{
// Cleanup:
// ... Delete the file that was created
try { File.Delete(fileName); } catch { /* Don't care */ }
}
}
[Fact]
public void PerformOpWithoutInit()
{
byte[] buf = new byte[10];
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
// If:
// ... I have a file stream wrapper that hasn't been initialized
// Then:
// ... Attempting to perform any operation will result in an exception
Assert.Throws<InvalidOperationException>(() => fsw.ReadData(buf, 1));
Assert.Throws<InvalidOperationException>(() => fsw.ReadData(buf, 1, 0));
Assert.Throws<InvalidOperationException>(() => fsw.WriteData(buf, 1));
Assert.Throws<InvalidOperationException>(() => fsw.Flush());
}
}
[Fact]
public void PerformWriteOpOnReadOnlyWrapper()
{
byte[] buf = new byte[10];
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
// If:
// ... I have a readonly file stream wrapper
// Then:
// ... Attempting to perform any write operation should result in an exception
Assert.Throws<InvalidOperationException>(() => fsw.WriteData(buf, 1));
Assert.Throws<InvalidOperationException>(() => fsw.Flush());
}
}
[Theory]
[InlineData(1024, 20, 10)] // Standard scenario
[InlineData(1024, 100, 100)] // Requested more bytes than there are
[InlineData(5, 20, 10)] // Internal buffer too small, force a move-to operation
public void ReadData(int internalBufferLength, int outBufferLength, int requestedBytes)
{
// Setup:
// ... I have a file that has a handful of bytes in it
string fileName = Path.GetTempFileName();
const string stringToWrite = "hello";
CreateTestFile(fileName, stringToWrite);
byte[] targetBytes = Encoding.Unicode.GetBytes(stringToWrite);
try
{
// If:
// ... I have a file stream wrapper that has been initialized to an existing file
// ... And I read some bytes from it
int bytesRead;
byte[] buf = new byte[outBufferLength];
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
fsw.Init(fileName, internalBufferLength, FileAccess.Read);
bytesRead = fsw.ReadData(buf, targetBytes.Length);
}
// Then:
// ... I should get those bytes back
Assert.Equal(targetBytes.Length, bytesRead);
Assert.True(targetBytes.Take(targetBytes.Length).SequenceEqual(buf.Take(targetBytes.Length)));
}
finally
{
// Cleanup:
// ... Delete the test file
CleanupTestFile(fileName);
}
}
[Theory]
[InlineData(1024)] // Standard scenario
[InlineData(10)] // Internal buffer too small, forces a flush
public void WriteData(int internalBufferLength)
{
string fileName = Path.GetTempFileName();
byte[] bytesToWrite = Encoding.Unicode.GetBytes("hello");
try
{
// If:
// ... I have a file stream that has been initialized
// ... And I write some bytes to it
using (FileStreamWrapper fsw = new FileStreamWrapper())
{
fsw.Init(fileName, internalBufferLength, FileAccess.ReadWrite);
int bytesWritten = fsw.WriteData(bytesToWrite, bytesToWrite.Length);
Assert.Equal(bytesToWrite.Length, bytesWritten);
}
// Then:
// ... The file I wrote to should contain only the bytes I wrote out
using (FileStream fs = File.OpenRead(fileName))
{
byte[] readBackBytes = new byte[1024];
int bytesRead = fs.Read(readBackBytes, 0, readBackBytes.Length);
Assert.Equal(bytesToWrite.Length, bytesRead); // If bytes read is not equal, then more or less of the original string was written to the file
Assert.True(bytesToWrite.SequenceEqual(readBackBytes.Take(bytesRead)));
}
}
finally
{
// Cleanup:
// ... Delete the test file
CleanupTestFile(fileName);
}
}
private static void CreateTestFile(string fileName, string value)
{
using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
byte[] bytesToWrite = Encoding.Unicode.GetBytes(value);
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
fs.Flush();
}
}
private static void CleanupTestFile(string fileName)
{
try { File.Delete(fileName); } catch { /* Don't Care */}
}
}
}

View File

@@ -0,0 +1,295 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.DataStorage
{
public class ReaderWriterPairTest
{
private static void VerifyReadWrite<T>(int valueLength, T value, Func<ServiceBufferFileStreamWriter, T, int> writeFunc, Func<ServiceBufferFileStreamReader, FileStreamReadResult<T>> readFunc)
{
// Setup: Create a mock file stream wrapper
Common.InMemoryWrapper mockWrapper = new Common.InMemoryWrapper();
try
{
// If:
// ... I write a type T to the writer
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(mockWrapper, "abc", 10, 10))
{
int writtenBytes = writeFunc(writer, value);
Assert.Equal(valueLength, writtenBytes);
}
// ... And read the type T back
FileStreamReadResult<T> outValue;
using (ServiceBufferFileStreamReader reader = new ServiceBufferFileStreamReader(mockWrapper, "abc"))
{
outValue = readFunc(reader);
}
// Then:
Assert.Equal(value, outValue.Value);
Assert.Equal(valueLength, outValue.TotalLength);
Assert.False(outValue.IsNull);
}
finally
{
// Cleanup: Close the wrapper
mockWrapper.Close();
}
}
[Theory]
[InlineData(0)]
[InlineData(10)]
[InlineData(-10)]
[InlineData(short.MaxValue)] // Two byte number
[InlineData(short.MinValue)] // Negative two byte number
public void Int16(short value)
{
VerifyReadWrite(sizeof(short) + 1, value, (writer, val) => writer.WriteInt16(val), reader => reader.ReadInt16(0));
}
[Theory]
[InlineData(0)]
[InlineData(10)]
[InlineData(-10)]
[InlineData(short.MaxValue)] // Two byte number
[InlineData(short.MinValue)] // Negative two byte number
[InlineData(int.MaxValue)] // Four byte number
[InlineData(int.MinValue)] // Negative four byte number
public void Int32(int value)
{
VerifyReadWrite(sizeof(int) + 1, value, (writer, val) => writer.WriteInt32(val), reader => reader.ReadInt32(0));
}
[Theory]
[InlineData(0)]
[InlineData(10)]
[InlineData(-10)]
[InlineData(short.MaxValue)] // Two byte number
[InlineData(short.MinValue)] // Negative two byte number
[InlineData(int.MaxValue)] // Four byte number
[InlineData(int.MinValue)] // Negative four byte number
[InlineData(long.MaxValue)] // Eight byte number
[InlineData(long.MinValue)] // Negative eight byte number
public void Int64(long value)
{
VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteInt64(val), reader => reader.ReadInt64(0));
}
[Theory]
[InlineData(0)]
[InlineData(10)]
public void Byte(byte value)
{
VerifyReadWrite(sizeof(byte) + 1, value, (writer, val) => writer.WriteByte(val), reader => reader.ReadByte(0));
}
[Theory]
[InlineData('a')]
[InlineData('1')]
[InlineData((char)0x9152)] // Test something in the UTF-16 space
public void Char(char value)
{
VerifyReadWrite(sizeof(char) + 1, value, (writer, val) => writer.WriteChar(val), reader => reader.ReadChar(0));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Boolean(bool value)
{
VerifyReadWrite(sizeof(bool) + 1, value, (writer, val) => writer.WriteBoolean(val), reader => reader.ReadBoolean(0));
}
[Theory]
[InlineData(0)]
[InlineData(10.1)]
[InlineData(-10.1)]
[InlineData(float.MinValue)]
[InlineData(float.MaxValue)]
[InlineData(float.PositiveInfinity)]
[InlineData(float.NegativeInfinity)]
public void Single(float value)
{
VerifyReadWrite(sizeof(float) + 1, value, (writer, val) => writer.WriteSingle(val), reader => reader.ReadSingle(0));
}
[Theory]
[InlineData(0)]
[InlineData(10.1)]
[InlineData(-10.1)]
[InlineData(float.MinValue)]
[InlineData(float.MaxValue)]
[InlineData(float.PositiveInfinity)]
[InlineData(float.NegativeInfinity)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
[InlineData(double.MinValue)]
[InlineData(double.MaxValue)]
public void Double(double value)
{
VerifyReadWrite(sizeof(double) + 1, value, (writer, val) => writer.WriteDouble(val), reader => reader.ReadDouble(0));
}
[Fact]
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 => reader.ReadSqlDecimal(0));
}
}
[Fact]
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 => reader.ReadDecimal(0));
}
}
[Fact]
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
};
foreach (DateTime value in testValues)
{
VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), reader => reader.ReadDateTime(0));
}
}
[Fact]
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
};
foreach (DateTimeOffset value in testValues)
{
VerifyReadWrite((sizeof(long) + 1)*2, value, (writer, val) => writer.WriteDateTimeOffset(val), reader => reader.ReadDateTimeOffset(0));
}
}
[Fact]
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 => reader.ReadTimeSpan(0));
}
}
[Fact]
public void StringNullTest()
{
// Setup: Create a mock file stream wrapper
Common.InMemoryWrapper mockWrapper = new Common.InMemoryWrapper();
// If:
// ... I write null as a string to the writer
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(mockWrapper, "abc", 10, 10))
{
// Then:
// ... I should get an argument null exception
Assert.Throws<ArgumentNullException>(() => writer.WriteString(null));
}
}
[Theory]
[InlineData(0, null)] // Test of empty string
[InlineData(1, new[] { 'j' })]
[InlineData(1, new[] { (char)0x9152 })]
[InlineData(100, new[] { 'j', (char)0x9152 })] // Test alternating utf-16/ascii characters
[InlineData(512, new[] { 'j', (char)0x9152 })] // Test that requires a 4 byte length
public void StringTest(int 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 => reader.ReadString(0));
}
[Fact]
public void BytesNullTest()
{
// Setup: Create a mock file stream wrapper
Common.InMemoryWrapper mockWrapper = new Common.InMemoryWrapper();
// If:
// ... I write null as a string to the writer
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(mockWrapper, "abc", 10, 10))
{
// Then:
// ... I should get an argument null exception
Assert.Throws<ArgumentNullException>(() => writer.WriteBytes(null, 0));
}
}
[Theory]
[InlineData(0, new byte[] { 0x00 })] // Test of empty byte[]
[InlineData(1, new byte[] { 0x00 })]
[InlineData(1, new byte[] { 0xFF })]
[InlineData(100, new byte[] { 0x10, 0xFF, 0x00 })]
[InlineData(512, new byte[] { 0x10, 0xFF, 0x00 })] // Test that requires a 4 byte length
public void Bytes(int length, byte[] values)
{
// Setup:
// ... Generate the test value
List<byte> sb = new List<byte>();
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, length), reader => reader.ReadBytes(0));
}
}
}