mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-26 17:24:21 -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:
@@ -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