mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
Export to Markdown Support (#1705)
* Adding file writer for Markdown tables. No testing yet. * Unit tests for the markdown writer * Wiring up the factory and and request types * Wiring up changes for Markdown serialization in serialization service * Couple last minute tweaks * Changes as per PR comments * Revert temp testing code. 🙈 * Fluent assertions in SerializationServiceTests.cs Co-authored-by: Ben Russell <russellben@microsoft.com>
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
|
||||
{
|
||||
public class SaveAsMarkdownFileStreamWriterTests
|
||||
{
|
||||
// Regex: Matches '|' not preceded by a '\'
|
||||
private static readonly Regex UnescapedPipe = new Regex(@"(?<!\\)\|", RegexOptions.Compiled);
|
||||
|
||||
[Test]
|
||||
public void Constructor_NullStream()
|
||||
{
|
||||
// Act
|
||||
TestDelegate action = () => _ = new SaveAsMarkdownFileStreamWriter(
|
||||
null,
|
||||
new SaveResultsAsMarkdownRequestParams(),
|
||||
Array.Empty<DbColumnWrapper>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.That(action, Throws.ArgumentNullException);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_NullColumns()
|
||||
{
|
||||
// Act
|
||||
TestDelegate action = () => _ = new SaveAsMarkdownFileStreamWriter(
|
||||
Stream.Null,
|
||||
new SaveResultsAsMarkdownRequestParams(),
|
||||
null
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.That(action, Throws.ArgumentNullException);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithoutSelectionWithHeader_WritesHeaderWithAllColumns()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that has no selection made, headers should be printed
|
||||
// ... Create a set of columns
|
||||
// --- Create a memory location to store the output
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams { IncludeHeaders = true };
|
||||
var (columns, _) = GetTestValues(2);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I construct a Markdown file writer
|
||||
using var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns);
|
||||
|
||||
// Then:
|
||||
// ... It should have written a line
|
||||
string[] lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines.Length, Is.EqualTo(2), "Expected two lines of output");
|
||||
|
||||
// ... It should have written a header line like |col1|col2|
|
||||
// ... It should have written a separator line like |---|---|
|
||||
ValidateLine(lines[0], columns.Select(c => c.ColumnName));
|
||||
ValidateLine(lines[1], Enumerable.Repeat("---", columns.Length));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithSelectionWithHeader_WritesHeaderWithSelectedColumns()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that has no selection made, headers should be printed
|
||||
// ... Create a set of columns
|
||||
// --- Create a memory location to store the output
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
IncludeHeaders = true,
|
||||
ColumnStartIndex = 1,
|
||||
ColumnEndIndex = 2,
|
||||
RowStartIndex = 0, // Including b/c it is required to be a "save selection"
|
||||
RowEndIndex = 10,
|
||||
};
|
||||
var (columns, _) = GetTestValues(4);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I construct a Markdown file writer
|
||||
using var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns);
|
||||
|
||||
// Then:
|
||||
// ... It should have written a line
|
||||
string[] lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines.Length, Is.EqualTo(2), "Expected two lines of output");
|
||||
|
||||
// ... It should have written a header line like |col1|col2|
|
||||
// ... It should have written a separator line like |---|---|
|
||||
ValidateLine(lines[0], columns.Skip(1).Take(2).Select(c => c.ColumnName));
|
||||
ValidateLine(lines[1], Enumerable.Repeat("---", 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithoutSelectionWithoutHeader_DoesNotWriteHeader()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that has no selection made, headers should not be printed
|
||||
// ... Create a set of columns
|
||||
// --- Create a memory location to store the output
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams { IncludeHeaders = false };
|
||||
var (columns, _) = GetTestValues(2);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I construct a Markdown file writer
|
||||
using var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns);
|
||||
|
||||
// Then:
|
||||
// ... It not have written anything
|
||||
string[] lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase("Something\rElse")] // Contains carriage return
|
||||
[TestCase("Something\nElse")] // Contains line feed
|
||||
[TestCase("Something\r\nElse")] // Contains carriage return
|
||||
public void EncodeMarkdownField_ContainsNewlineCharacters_ShouldConvertToBr(string field)
|
||||
{
|
||||
// If: I Markdown encode a field that has a newline
|
||||
string output = SaveAsMarkdownFileStreamWriter.EncodeMarkdownField(field);
|
||||
|
||||
// Then: It should replace it the newline character(s) with a <br />
|
||||
const string expected = "Something<br />Else";
|
||||
Assert.That(output, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EncodeMarkdownField_ContainsDelimiter_ShouldBeEscaped()
|
||||
{
|
||||
// If: I Markdown encode a field that has a pipe in it
|
||||
const string input = "|Something|Else|";
|
||||
string output = SaveAsMarkdownFileStreamWriter.EncodeMarkdownField(input);
|
||||
|
||||
// Then: It should escape the pipe character
|
||||
const string expected = @"\|Something\|Else\|";
|
||||
Assert.AreEqual(expected, output);
|
||||
}
|
||||
|
||||
// @TODO: Convert excess whitespace to on user choice
|
||||
// [TestCase("\tSomething")] // Starts with tab
|
||||
// [TestCase("Something\t")] // Ends with tab
|
||||
// [TestCase(" Something")] // Starts with space
|
||||
// [TestCase("Something ")] // Ends with space
|
||||
// [TestCase(" Something ")] // Starts and ends with space
|
||||
// [TestCase("Something else")] // Contains multiple consecutive spaces
|
||||
// public void EncodeMarkdownField_WhitespaceAtFrontOrBack_ShouldBeWrapped(string field)
|
||||
// {
|
||||
// // Setup: Create MarkdownFileStreamWriter that specifies the text identifier and field separator
|
||||
// var writer = GetWriterForEncodingTests(null, null, null);
|
||||
//
|
||||
// // If: I Markdown encode a field that has forbidden characters in it
|
||||
// string output = writer.EncodeMarkdownField(field);
|
||||
//
|
||||
// // Then: It should wrap it in quotes
|
||||
// Assert.True(Regex.IsMatch(output, "^\".*\"$", RegexOptions.Singleline));
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void EncodeMarkdownField_ContainsHtmlEntityCharacters_ShouldConvertToHtmlEntities()
|
||||
{
|
||||
// If: I Markdown encode a field that has html entity characters in it
|
||||
const string input = "<<>>&®±ßüÁ";
|
||||
string output = SaveAsMarkdownFileStreamWriter.EncodeMarkdownField(input);
|
||||
|
||||
// Then: The entity characters should be HTML encoded
|
||||
const string expected = "<<>>&®±ßüÁ";
|
||||
Assert.That(output, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EncodeMarkdownField_Null()
|
||||
{
|
||||
// If: I Markdown encode a null
|
||||
string output = SaveAsMarkdownFileStreamWriter.EncodeMarkdownField(null);
|
||||
|
||||
// Then: there should be a string version of null returned
|
||||
Assert.That(output, Is.EqualTo("NULL"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteRow_WithoutColumnSelection()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that has no selection made or header enabled
|
||||
// ... Create a set of data to write
|
||||
// ... Create a memory location to store the data
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams();
|
||||
var (columns, data) = GetTestValues(2);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I write a row
|
||||
using (var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns))
|
||||
{
|
||||
writer.WriteRow(data, columns);
|
||||
}
|
||||
|
||||
// Then: It should write one line with the two cells
|
||||
string[] lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines.Length, Is.EqualTo(1), "Expected one line of output");
|
||||
|
||||
ValidateLine(lines[0], data.Select(c => c.DisplayValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteRow_WithColumnSelection()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that selects n-1 columns from the front and back
|
||||
// ... Create a set of data to write
|
||||
// ... Create a memory location to store the data
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
ColumnStartIndex = 1,
|
||||
ColumnEndIndex = 2,
|
||||
RowStartIndex = 0, // Including b/c it is required to be a "save selection"
|
||||
RowEndIndex = 10
|
||||
};
|
||||
var (columns, data) = GetTestValues(4);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I write a row
|
||||
using (var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns))
|
||||
{
|
||||
writer.WriteRow(data, columns);
|
||||
}
|
||||
|
||||
// Then:
|
||||
// ... It should have written one line with the two cells written
|
||||
var lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines.Length, Is.EqualTo(1), "Expected one line of output");
|
||||
|
||||
ValidateLine(lines[0], data.Skip(1).Take(2).Select(c => c.DisplayValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteRow_EncodingTest()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create default request params
|
||||
// ... Create a set of data to write that contains characters that should be encoded
|
||||
// ... Create a memory location to store the data
|
||||
// @TODO: Add case to test string for non-breaking spaces
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams();
|
||||
var columns = new[] { new DbColumnWrapper(new TestDbColumn("column")) };
|
||||
var data = new[] { new DbCellValue { DisplayValue = "|Something|\n|<<>>&|" } };
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I write a row
|
||||
using (var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns))
|
||||
{
|
||||
writer.WriteRow(data, columns);
|
||||
}
|
||||
|
||||
// Then:
|
||||
// ... It should have written one line with the data properly encoded
|
||||
string[] lines = ParseWriterOutput(output, Environment.NewLine);
|
||||
Assert.That(lines.Length, Is.EqualTo(1), "Expected one line of output");
|
||||
|
||||
ValidateLine(lines[0], new[] { "\\|Something\\|<br />\\|<<>>&\\|" });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteRow_CustomLineSeparator()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that has custom line separator
|
||||
// ... Create a set of data to write
|
||||
// ... Create a memory location to store the data
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
LineSeparator = "$$",
|
||||
IncludeHeaders = true,
|
||||
};
|
||||
var (columns, data) = GetTestValues(2);
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I set write a row
|
||||
using (var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns))
|
||||
{
|
||||
writer.WriteRow(data, columns);
|
||||
}
|
||||
|
||||
// Then:
|
||||
// ... The lines should be split by the custom line separator
|
||||
var lines = ParseWriterOutput(output, "$$");
|
||||
Assert.That(lines.Length, Is.EqualTo(3), "Expected three lines of output");
|
||||
|
||||
// Note: Header output has been tested in constructor tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteRow_CustomEncoding()
|
||||
{
|
||||
// Setup:
|
||||
// ... Create a request params that uses a custom encoding
|
||||
// ... Create a set of data to write
|
||||
// ... Create a memory location to store the data
|
||||
var requestParams = new SaveResultsAsMarkdownRequestParams { Encoding = "utf-16", };
|
||||
var data = new[] { new DbCellValue { DisplayValue = "ü" } };
|
||||
var columns = new[] { new DbColumnWrapper(new TestDbColumn("column1")) };
|
||||
byte[] output = new byte[8192];
|
||||
using var outputStream = new MemoryStream(output);
|
||||
|
||||
// If: I write a row
|
||||
using (var writer = new SaveAsMarkdownFileStreamWriter(outputStream, requestParams, columns))
|
||||
{
|
||||
writer.WriteRow(data, columns);
|
||||
}
|
||||
|
||||
// Then:
|
||||
// ... It should have written the umlaut as an HTML entity in utf-16le
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
string outputString = Encoding.Unicode.GetString(output).TrimEnd('\0', '\r', '\n');
|
||||
Assert.That(outputString, Is.EqualTo("|ü|"));
|
||||
}
|
||||
|
||||
private static (DbColumnWrapper[] columns, DbCellValue[] cells) GetTestValues(int columnCount)
|
||||
{
|
||||
var data = new DbCellValue[columnCount];
|
||||
var columns = new DbColumnWrapper[columnCount];
|
||||
for (int i = 0; i < columnCount; i++)
|
||||
{
|
||||
data[i] = new DbCellValue { DisplayValue = $"item{i}" };
|
||||
columns[i] = new DbColumnWrapper(new TestDbColumn($"column{i}"));
|
||||
}
|
||||
return (columns, data);
|
||||
}
|
||||
|
||||
private static string[] ParseWriterOutput(byte[] output, string lineSeparator)
|
||||
{
|
||||
string outputString = Encoding.UTF8.GetString(output).Trim('\0');
|
||||
string[] lines = outputString.Split(lineSeparator);
|
||||
|
||||
// Make sure the file ends with a new line and return all but the meaningful lines
|
||||
Assert.That(lines[^1], Is.Empty, "Output did not end with a newline");
|
||||
return lines.Take(lines.Length - 1).ToArray();
|
||||
}
|
||||
|
||||
private static void ValidateLine(string line, IEnumerable<string> expectedCells)
|
||||
{
|
||||
string[] cells = UnescapedPipe.Split(line);
|
||||
string[] expectedCellsArray = expectedCells as string[] ?? expectedCells.ToArray();
|
||||
Assert.That(cells.Length - 2, Is.EqualTo(expectedCellsArray.Length), "Wrong number of cells in output");
|
||||
|
||||
Assert.That(cells[0], Is.Empty, "Row did not start with |");
|
||||
Assert.That(cells[^1], Is.Empty, "Row did not end with |");
|
||||
|
||||
for (int i = 0; i < expectedCellsArray.Length; i++)
|
||||
{
|
||||
Assert.That(cells[i + 1], Is.EqualTo(expectedCellsArray[i]), "Wrong cell value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,19 +60,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
protected Mock<IProtocolEndpoint> HostMock { get; private set; }
|
||||
protected SerializationService SerializationService { get; private set; }
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsAsCsvNoHeaderSuccess()
|
||||
{
|
||||
await TestSaveAsCsvSuccess(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsAsCsvWithHeaderSuccess()
|
||||
{
|
||||
await TestSaveAsCsvSuccess(true);
|
||||
}
|
||||
|
||||
private async Task TestSaveAsCsvSuccess(bool includeHeaders)
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task TestSaveAsCsvSuccess(bool includeHeaders)
|
||||
{
|
||||
await this.RunFileSaveTest(async (filePath) =>
|
||||
{
|
||||
@@ -102,18 +92,47 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsAsCsvNoHeaderMultiRequestSuccess()
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public Task TestSaveAsMarkdownSuccess(bool includeHeaders)
|
||||
{
|
||||
await TestSaveAsCsvMultiRequestSuccess(false);
|
||||
return this.RunFileSaveTest(async filePath =>
|
||||
{
|
||||
// Give:
|
||||
// ... A simple data set that requires 1 message
|
||||
var saveParams = new SerializeDataStartRequestParams
|
||||
{
|
||||
FilePath = filePath,
|
||||
Columns = DefaultColumns,
|
||||
Rows = DefaultData,
|
||||
IsLastBatch = true,
|
||||
SaveFormat = "markdown",
|
||||
IncludeHeaders = includeHeaders,
|
||||
};
|
||||
|
||||
// When: I attempt to save this to a file
|
||||
var efv = new EventFlowValidator<SerializeDataResult>()
|
||||
.AddStandardResultValidator()
|
||||
.Complete();
|
||||
|
||||
await SerializationService.RunSerializeStartRequest(saveParams, efv.Object);
|
||||
|
||||
// Then:
|
||||
// ... There should not have been any errors
|
||||
efv.Validate();
|
||||
|
||||
// ... And the file should look as expected
|
||||
VerifyContents.VerifyMarkdownMatchesData(
|
||||
saveParams.Rows,
|
||||
saveParams.Columns,
|
||||
saveParams.IncludeHeaders,
|
||||
saveParams.FilePath);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsAsCsvWithHeaderMultiRequestSuccess()
|
||||
{
|
||||
await TestSaveAsCsvMultiRequestSuccess(true);
|
||||
}
|
||||
private async Task TestSaveAsCsvMultiRequestSuccess(bool includeHeaders)
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task TestSaveAsCsvMultiRequestSuccess(bool includeHeaders)
|
||||
{
|
||||
Action<SerializeDataStartRequestParams> setParams = (serializeParams) => {
|
||||
serializeParams.SaveFormat = "csv";
|
||||
@@ -149,6 +168,22 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
await this.TestSerializeDataMultiRequestSuccess(setParams, validation);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task SaveAsMarkdownMultiRequestSuccess(bool includeHeaders)
|
||||
{
|
||||
Action<SerializeDataStartRequestParams> setParams = serializeParams =>
|
||||
{
|
||||
serializeParams.SaveFormat = "markdown";
|
||||
serializeParams.IncludeHeaders = includeHeaders;
|
||||
};
|
||||
Action<string> validation = filePath =>
|
||||
{
|
||||
VerifyContents.VerifyMarkdownMatchesData(DefaultData, DefaultColumns, includeHeaders, filePath);
|
||||
};
|
||||
await this.TestSerializeDataMultiRequestSuccess(setParams, validation);
|
||||
}
|
||||
|
||||
private async Task TestSerializeDataMultiRequestSuccess(Action<SerializeDataStartRequestParams> setStandardParams, Action<string> verify)
|
||||
{
|
||||
await this.RunFileSaveTest(async (filePath) =>
|
||||
@@ -225,9 +260,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
{
|
||||
return efv.AddResultValidation(r =>
|
||||
{
|
||||
Assert.NotNull(r);
|
||||
Assert.Null(r.Messages);
|
||||
Assert.True(r.Succeeded);
|
||||
Assert.That(r, Is.Not.Null, "Result should not be null");
|
||||
Assert.That(r.Messages, Is.Null, "No messages should be attached to the result");
|
||||
Assert.That(r.Succeeded, Is.True, "Result should indicate request succeeded");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -236,10 +271,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
{
|
||||
public static void VerifyCsvMatchesData(DbCellValue[][] data, ColumnInfo[] columns, bool includeHeaders, string filePath)
|
||||
{
|
||||
Assert.True(File.Exists(filePath), "Expected file to have been written");
|
||||
Assert.That(filePath, Does.Exist, "Expected file to have been written");
|
||||
string[] lines = File.ReadAllLines(filePath);
|
||||
int expectedLength = includeHeaders ? data.Length + 1 : data.Length;
|
||||
Assert.AreEqual(expectedLength, lines.Length);
|
||||
Assert.That(lines.Length, Is.EqualTo(expectedLength), "Incorrect number of lines in result");
|
||||
int lineIndex = 0;
|
||||
if (includeHeaders)
|
||||
{
|
||||
@@ -248,7 +283,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
}
|
||||
for (int dataIndex =0; dataIndex < data.Length && lineIndex < lines.Length; dataIndex++, lineIndex++)
|
||||
{
|
||||
AssertLineEquals(lines[lineIndex], data[dataIndex].Select((d) => GetCsvPrintValue(d)).ToArray());
|
||||
AssertLineEquals(lines[lineIndex], data[dataIndex].Select(GetCsvPrintValue).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,17 +295,54 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
private static void AssertLineEquals(string line, string[] expected)
|
||||
{
|
||||
var actual = line.Split(',');
|
||||
Assert.True(actual.Length == expected.Length, $"Line '{line}' does not match values {string.Join(",", expected)}");
|
||||
Assert.That(actual.Length, Is.EqualTo(expected.Length),
|
||||
$"Line '{line}' does not match values {string.Join(",", expected)}");
|
||||
for (int i = 0; i < actual.Length; i++)
|
||||
{
|
||||
Assert.True(expected[i] == actual[i], $"Line '{line}' does not match values '{string.Join(",", expected)}' as '{expected[i]}' does not equal '{actual[i]}'");
|
||||
Assert.That(actual[i], Is.EqualTo(expected[i]),
|
||||
$"Line '{line}' does not match values '{string.Join(",", expected)}' as '{expected[i]}' does not equal '{actual[i]}'");
|
||||
}
|
||||
}
|
||||
|
||||
public static void VerifyMarkdownMatchesData(
|
||||
DbCellValue[][] data,
|
||||
ColumnInfo[] columns,
|
||||
bool includeHeaders,
|
||||
string filePath)
|
||||
{
|
||||
Assert.That(filePath, Does.Exist, "Expected file to be written");
|
||||
string[] lines = File.ReadAllLines(filePath);
|
||||
|
||||
int expectedLength = includeHeaders ? data.Length + 2 : data.Length;
|
||||
Assert.That(lines.Length, Is.EqualTo(expectedLength), "Incorrect number of lines in output");
|
||||
|
||||
int lineOffset = 0;
|
||||
if (includeHeaders)
|
||||
{
|
||||
// First line is |col1|col2|...
|
||||
var firstLineExpected = $"|{string.Join("|", columns.Select(c => c.Name))}|";
|
||||
Assert.That(lines[0], Is.EqualTo(firstLineExpected), "Header row does not match expected");
|
||||
// Second line is |---|---|...
|
||||
var secondLineExpected = $"|{string.Join("", Enumerable.Repeat("---|", columns.Length))}";
|
||||
Assert.That(lines[1], Is.EqualTo(secondLineExpected), "Separator row does not match expected");
|
||||
|
||||
lineOffset = 2;
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var expectedLine = $"|{string.Join("|", data[i].Select(GetMarkdownPrintValue).ToArray())}|";
|
||||
Assert.That(lines[i + lineOffset], Is.EqualTo(expectedLine), "Data row does not match expected");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetMarkdownPrintValue(DbCellValue d) =>
|
||||
d.IsNull ? "NULL" : d.DisplayValue;
|
||||
|
||||
public static void VerifyJsonMatchesData(DbCellValue[][] data, ColumnInfo[] columns, string filePath)
|
||||
{
|
||||
// ... Upon deserialization to an array of dictionaries
|
||||
Assert.True(File.Exists(filePath), "Expected file to have been written");
|
||||
// ... Upon deserialization to an array of dictionaries
|
||||
Assert.That(filePath, Does.Exist, "Expected file to have been written");
|
||||
string output = File.ReadAllText(filePath);
|
||||
Dictionary<string, object>[] outputObject =
|
||||
JsonConvert.DeserializeObject<Dictionary<string, object>[]>(output);
|
||||
@@ -278,18 +350,18 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
// ... There should be 2 items in the array,
|
||||
// ... The item should have three fields, and three values, assigned appropriately
|
||||
// ... The deserialized values should match the display value
|
||||
Assert.AreEqual(data.Length, outputObject.Length);
|
||||
Assert.That(outputObject.Length, Is.EqualTo(data.Length), "Incorrect number of records in output");
|
||||
for (int rowIndex = 0; rowIndex < outputObject.Length; rowIndex++)
|
||||
{
|
||||
Dictionary<string,object> item = outputObject[rowIndex];
|
||||
Assert.AreEqual(columns.Length, item.Count);
|
||||
Assert.That(item.Count, Is.EqualTo(columns.Length), $"Incorrect number of cells for record {rowIndex}");
|
||||
for (int columnIndex = 0; columnIndex < columns.Length; columnIndex++)
|
||||
{
|
||||
var key = columns[columnIndex].Name;
|
||||
Assert.True(item.ContainsKey(key));
|
||||
Assert.That(item, Contains.Key(key), $"Record {rowIndex} does not contain column {key}");
|
||||
DbCellValue value = data[rowIndex][columnIndex];
|
||||
object expectedValue = GetJsonExpectedValue(value, columns[columnIndex]);
|
||||
Assert.AreEqual(expectedValue, item[key]);
|
||||
Assert.That(item[key], Is.EqualTo(expectedValue), $"Record {rowIndex}, column {key} contains incorrect value");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,8 +385,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
}
|
||||
public static void VerifyXmlMatchesData(DbCellValue[][] data, ColumnInfo[] columns, string filePath)
|
||||
{
|
||||
// ... Upon deserialization to an array of dictionaries
|
||||
Assert.True(File.Exists(filePath), "Expected file to have been written");
|
||||
// ... Upon deserialization to an array of dictionaries
|
||||
Assert.That(filePath, Does.Exist, "Expected file to have been written");
|
||||
string output = File.ReadAllText(filePath);
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(output);
|
||||
@@ -325,7 +397,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
string xpath = "data/row";
|
||||
var rows = xmlDoc.SelectNodes(xpath);
|
||||
|
||||
Assert.AreEqual(data.Length, rows.Count);
|
||||
Assert.That(rows.Count, Is.EqualTo(data.Length), "Incorrect number of records in output");
|
||||
for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var rowValue = rows.Item(rowIndex);
|
||||
@@ -335,10 +407,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
{
|
||||
var columnName = columns[columnIndex].Name;
|
||||
var xmlColumn = xmlCols.FirstOrDefault(x => x.Name == columnName);
|
||||
Assert.NotNull(xmlColumn);
|
||||
Assert.That(xmlColumn, Is.Not.Null, $"Record {rowIndex} does not contain column {columnName}");
|
||||
DbCellValue value = data[rowIndex][columnIndex];
|
||||
object expectedValue = GetXmlExpectedValue(value);
|
||||
Assert.AreEqual(expectedValue, xmlColumn.InnerText);
|
||||
Assert.That(xmlColumn.InnerText, Is.EqualTo(expectedValue), $"Invalid value for record {rowIndex}, column {columnName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,129 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
|
||||
#endregion
|
||||
|
||||
#region Markdown Tests
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsMarkdown_NonExistentQuery()
|
||||
{
|
||||
// Given: A working query and workspace service
|
||||
WorkspaceService<SqlToolsSettings> ws = Common.GetPrimedWorkspaceService(null);
|
||||
QueryExecutionService qes = Common.GetPrimedExecutionService(null, false, false, false, ws);
|
||||
|
||||
// If: I attempt to save a result set from a query that doesn't exist
|
||||
var saveParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
OwnerUri = Constants.OwnerUri, // Won't exist because nothing has executed
|
||||
};
|
||||
var evf = new EventFlowValidator<SaveResultRequestResult>()
|
||||
.AddStandardErrorValidation()
|
||||
.Complete();
|
||||
await qes.HandleSaveResultsAsMarkdownRequest(saveParams, evf.Object);
|
||||
|
||||
// Then:
|
||||
// ... An error event should have been fired
|
||||
// ... No success event should have been fired
|
||||
evf.Validate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultAsMarkdown_Failure()
|
||||
{
|
||||
// Given:
|
||||
// ... A working query and workspace service
|
||||
WorkspaceService<SqlToolsSettings> ws = Common.GetPrimedWorkspaceService(Constants.StandardQuery);
|
||||
QueryExecutionService qes = Common.GetPrimedExecutionService(
|
||||
Common.ExecutionPlanTestDataSet,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
ws,
|
||||
out ConcurrentDictionary<string, byte[]> storage);
|
||||
|
||||
// ... The query execution service has executed a query with results
|
||||
var executeParams = new ExecuteDocumentSelectionParams { QuerySelection = null, OwnerUri = Constants.OwnerUri };
|
||||
var executeRequest = RequestContextMocks.Create<ExecuteRequestResult>(null);
|
||||
await qes.HandleExecuteRequest(executeParams, executeRequest.Object);
|
||||
await qes.WorkTask;
|
||||
await qes.ActiveQueries[Constants.OwnerUri].ExecutionTask;
|
||||
|
||||
// If: I attempt to save a result set and get it to throw because of invalid column selection
|
||||
var saveParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
BatchIndex = 0,
|
||||
FilePath = "qqq",
|
||||
OwnerUri = Constants.OwnerUri,
|
||||
ResultSetIndex = 0,
|
||||
ColumnStartIndex = -1,
|
||||
ColumnEndIndex = 100,
|
||||
RowStartIndex = 0,
|
||||
RowEndIndex = 5
|
||||
};
|
||||
qes.MarkdownFileFactory = GetMarkdownStreamFactory(storage, saveParams);
|
||||
var efv = new EventFlowValidator<SaveResultRequestResult>()
|
||||
.AddStandardErrorValidation()
|
||||
.Complete();
|
||||
|
||||
await qes.HandleSaveResultsAsMarkdownRequest(saveParams, efv.Object);
|
||||
await qes.ActiveQueries[saveParams.OwnerUri]
|
||||
.Batches[saveParams.BatchIndex]
|
||||
.ResultSets[saveParams.ResultSetIndex]
|
||||
.SaveTasks[saveParams.FilePath];
|
||||
|
||||
// Then:
|
||||
// ... An error event should have been fired
|
||||
// ... No success event should have been fired
|
||||
efv.Validate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveResultsAsMarkdown_Success()
|
||||
{
|
||||
// Given:
|
||||
// ... A working query and workspace service
|
||||
WorkspaceService<SqlToolsSettings> ws = Common.GetPrimedWorkspaceService(Constants.StandardQuery);
|
||||
QueryExecutionService qes = Common.GetPrimedExecutionService(
|
||||
Common.ExecutionPlanTestDataSet,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
ws,
|
||||
out ConcurrentDictionary<string, byte[]> storage);
|
||||
|
||||
// ... The query execution service has executed a query with results
|
||||
var executeParams = new ExecuteDocumentSelectionParams { QuerySelection = null, OwnerUri = Constants.OwnerUri };
|
||||
var executeRequest = RequestContextMocks.Create<ExecuteRequestResult>(null);
|
||||
await qes.HandleExecuteRequest(executeParams, executeRequest.Object);
|
||||
await qes.WorkTask;
|
||||
await qes.ActiveQueries[Constants.OwnerUri].ExecutionTask;
|
||||
|
||||
// If: I attempt to save a result set from a query
|
||||
var saveParams = new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
OwnerUri = Constants.OwnerUri,
|
||||
FilePath = "qqq",
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0
|
||||
};
|
||||
qes.MarkdownFileFactory = GetMarkdownStreamFactory(storage, saveParams);
|
||||
var efv = new EventFlowValidator<SaveResultRequestResult>()
|
||||
.AddStandardResultValidator()
|
||||
.Complete();
|
||||
|
||||
await qes.HandleSaveResultsAsMarkdownRequest(saveParams, efv.Object);
|
||||
await qes.ActiveQueries[saveParams.OwnerUri]
|
||||
.Batches[saveParams.BatchIndex]
|
||||
.ResultSets[saveParams.ResultSetIndex]
|
||||
.SaveTasks[saveParams.FilePath];
|
||||
|
||||
// Then:
|
||||
// ... I should have a successful result
|
||||
// ... There should not have been an error
|
||||
efv.Validate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XML tests
|
||||
|
||||
[Test]
|
||||
@@ -507,6 +630,23 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
private static IFileStreamFactory GetMarkdownStreamFactory(
|
||||
IDictionary<string, byte[]> storage,
|
||||
SaveResultsAsMarkdownRequestParams saveParams)
|
||||
{
|
||||
var mock = new Mock<IFileStreamFactory>();
|
||||
mock.Setup(fsf => fsf.GetReader(It.IsAny<string>()))
|
||||
.Returns<string>(output => new ServiceBufferFileStreamReader(new MemoryStream(storage[output]), new QueryExecutionSettings()));
|
||||
mock.Setup(fsf => fsf.GetWriter(It.IsAny<string>(), It.IsAny<IReadOnlyList<DbColumnWrapper>>()))
|
||||
.Returns<string, IReadOnlyList<DbColumnWrapper>>((output, columns) =>
|
||||
{
|
||||
storage.Add(output, new byte[8192]);
|
||||
return new SaveAsMarkdownFileStreamWriter(new MemoryStream(storage[output]), saveParams, columns);
|
||||
});
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
private static IFileStreamFactory GetXmlStreamFactory(
|
||||
IDictionary<string, byte[]> storage,
|
||||
SaveResultsAsXmlRequestParams saveParams)
|
||||
|
||||
Reference in New Issue
Block a user