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:
Benjamin Russell
2022-09-27 13:55:43 -05:00
committed by GitHub
parent 5c20f92312
commit af2c0c77e7
10 changed files with 905 additions and 54 deletions

View File

@@ -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 &nbsp; 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 = "&lt;&lt;&gt;&gt;&amp;&#174;&#177;&#223;&#252;&#193;";
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 />\\|&lt;&lt;&gt;&gt;&amp;\\|" });
}
[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("|&#252;|"));
}
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");
}
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -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)