From e71bcefb280edf87f484edb651aec280e834251c Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 10 Jan 2017 16:42:03 -0800 Subject: [PATCH] Feature: Progressive Messages (#208) This change is a reworking of the way that messages are sent to clients from the service layer. It is also a reworking of the protocol to ensure that all formulations of query send back events to the client in a deterministic ordering. To support the first change: * Added a new event that will be sent when a message is generated * Messages now indicate which Batch (if any) generated them * Messages now indicate if they were error level * Removed message storage in Batch objects and BatchSummary objects * Batch objects no longer have error state --- .../QueryExecution/Batch.cs | 101 ++-- .../QueryExecution/Contracts/BatchSummary.cs | 10 - .../QueryExecuteCompleteNotification.cs | 5 - .../QueryExecuteMessageNotification.cs | 32 ++ .../Contracts/QueryExecuteRequest.cs | 4 - .../QueryExecution/Contracts/ResultMessage.cs | 24 +- .../QueryExecution/Query.cs | 14 + .../QueryExecution/QueryExecutionService.cs | 18 +- .../Connection/ReliableConnectionTests.cs | 3 - .../Tests/QueryExecutionTests.cs | 2 +- .../QueryExecution/Common.cs | 10 + .../QueryExecution/Execution/BatchTests.cs | 444 ++++++++---------- .../QueryExecution/Execution/QueryTests.cs | 263 ++++++----- .../Execution/ServiceIntegrationTests.cs | 72 +-- .../Tests/QueryExecutionTests.cs | 41 +- .../Tests/TestHelper.cs | 11 +- 16 files changed, 545 insertions(+), 509 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteMessageNotification.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index 0ee7d550..718eea7b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -40,16 +40,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// private DateTime executionStartTime; + /// + /// Whether or not any messages have been sent + /// + private bool messagesSent; + /// /// Factory for creating readers/writers for the output of the batch /// private readonly IFileStreamFactory outputFileFactory; - /// - /// Internal representation of the messages so we can modify internally - /// - internal readonly List resultMessages; - /// /// Internal representation of the result sets so we can modify internally /// @@ -71,7 +71,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution HasExecuted = false; Id = ordinalId; resultSets = new List(); - resultMessages = new List(); this.outputFileFactory = outputFileFactory; } @@ -83,11 +82,22 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The batch that completed public delegate Task BatchAsyncEventHandler(Batch batch); + /// + /// Asynchronous handler for when a message is emitted by the sql connection + /// + /// The message that was emitted + public delegate Task BatchAsyncMessageHandler(ResultMessage message); + /// /// Event that will be called when the batch has completed execution /// public event BatchAsyncEventHandler BatchCompletion; + /// + /// Event that will be called when a message has been emitted + /// + public event BatchAsyncMessageHandler BatchMessageSent; + /// /// Event to call when the batch has started execution /// @@ -132,11 +142,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public string ExecutionStartTimeStamp { get { return executionStartTime.ToString("o"); } } - /// - /// Whether or not this batch has an error - /// - public bool HasError { get; set; } - /// /// Whether or not this batch has been executed, regardless of success or failure /// @@ -147,14 +152,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public int Id { get; private set; } - /// - /// Messages that have come back from the server - /// - public IEnumerable ResultMessages - { - get { return resultMessages; } - } - /// /// The result sets of the batch execution /// @@ -187,7 +184,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Batch summary with information available at start BatchSummary summary = new BatchSummary { - HasError = HasError, Id = Id, Selection = Selection, ExecutionStart = ExecutionStartTimeStamp @@ -197,7 +193,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution if (HasExecuted) { summary.ResultSetSummaries = ResultSummaries; - summary.Messages = ResultMessages.ToArray(); summary.ExecutionEnd = ExecutionEndTimeStamp; summary.ExecutionElapsed = ExecutionElapsedTime; } @@ -244,7 +239,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { // Register the message listener to *this instance* of the batch // Note: This is being done to associate messages with batches - sqlConn.GetUnderlyingConnection().InfoMessage += StoreDbMessage; + sqlConn.GetUnderlyingConnection().InfoMessage += ServerMessageHandler; command = sqlConn.GetUnderlyingConnection().CreateCommand(); // Add a handler for when the command completes @@ -298,27 +293,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // If there were no messages, for whatever reason (NO COUNT set, messages // were emitted, records returned), output a "successful" message - if (resultMessages.Count == 0) + if (!messagesSent) { - resultMessages.Add(new ResultMessage(SR.QueryServiceCompletedSuccessfully)); + await SendMessage(SR.QueryServiceCompletedSuccessfully, false); } } } } catch (DbException dbe) { - HasError = true; - UnwrapDbException(dbe); + await UnwrapDbException(dbe); } catch (TaskCanceledException) { - resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled)); + await SendMessage(SR.QueryServiceQueryCancelled, false); throw; } catch (Exception e) { - HasError = true; - resultMessages.Add(new ResultMessage(SR.QueryServiceQueryFailed(e.Message))); + await SendMessage(SR.QueryServiceQueryFailed(e.Message), true); throw; } finally @@ -327,7 +320,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution ReliableSqlConnection sqlConn = conn as ReliableSqlConnection; if (sqlConn != null) { - sqlConn.GetUnderlyingConnection().InfoMessage -= StoreDbMessage; + sqlConn.GetUnderlyingConnection().InfoMessage -= ServerMessageHandler; } // Mark that we have executed @@ -400,6 +393,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution #region Private Helpers + private async Task SendMessage(string message, bool isError) + { + // If the message event is null, this is a no-op + if (BatchMessageSent == null) + { + return; + } + + // State that we've sent any message, and send it + messagesSent = true; + await BatchMessageSent(new ResultMessage(message, isError, Id)); + } + /// /// Handler for when the StatementCompleted event is fired for this batch's command. This /// will be executed ONLY when there is a rowcount to report. If this event is not fired @@ -410,17 +416,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution internal void StatementCompletedHandler(object sender, StatementCompletedEventArgs args) { // Add a message for the number of rows the query returned - string message; - if (args.RecordCount == 1) - { - message = SR.QueryServiceAffectedOneRow; - } - else - { - message = SR.QueryServiceAffectedRows(args.RecordCount); - } - - resultMessages.Add(new ResultMessage(message)); + string message = args.RecordCount == 1 + ? SR.QueryServiceAffectedOneRow + : SR.QueryServiceAffectedRows(args.RecordCount); + SendMessage(message, false).Wait(); } /// @@ -430,30 +429,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// Object that fired the event /// Arguments from the event - private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args) + private void ServerMessageHandler(object sender, SqlInfoMessageEventArgs args) { - resultMessages.Add(new ResultMessage(args.Message)); + SendMessage(args.Message, false).Wait(); } /// - /// Attempts to convert a to a that + /// Attempts to convert an to a that /// contains much more info about Sql Server errors. The exception is then unwrapped and - /// messages are formatted and stored in . If the exception - /// cannot be converted to SqlException, the message is written to the messages list. + /// messages are formatted and sent to the extension. If the exception cannot be + /// converted to SqlException, the message is written to the messages list. /// /// The exception to unwrap - internal void UnwrapDbException(DbException dbe) + private async Task UnwrapDbException(Exception dbe) { SqlException se = dbe as SqlException; if (se != null) { var errors = se.Errors.Cast().ToList(); + // Detect user cancellation errors if (errors.Any(error => error.Class == 11 && error.Number == 0)) { // User cancellation error, add the single message - HasError = false; - resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled)); + await SendMessage(SR.QueryServiceQueryCancelled, false); } else { @@ -464,13 +463,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution string message = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}", error.Number, error.Class, error.State, lineNumber, Environment.NewLine, error.Message); - resultMessages.Add(new ResultMessage(message)); + await SendMessage(message, true); } } } else { - resultMessages.Add(new ResultMessage(dbe.Message)); + await SendMessage(dbe.Message, true); } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs index 884d76f6..f9404039 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs @@ -25,11 +25,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string ExecutionStart { get; set; } - /// - /// Whether or not the batch was successful. True indicates errors, false indicates success - /// - public bool HasError { get; set; } - /// /// The ID of the result set within the query results /// @@ -40,11 +35,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public SelectionData Selection { get; set; } - /// - /// Any messages that came back from the server during execution of the batch - /// - public ResultMessage[] Messages { get; set; } - /// /// The summaries of the result sets inside the batch /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs index 8375235a..90c8c7b3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs @@ -21,11 +21,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// Summaries of the result sets that were returned with the query /// public BatchSummary[] BatchSummaries { get; set; } - - /// - /// Error message, if any - /// - public string Message { get; set; } } public class QueryExecuteCompleteEvent diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteMessageNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteMessageNotification.cs new file mode 100644 index 00000000..e65f90d0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteMessageNotification.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Parameters to be sent back with a message notification + /// + public class QueryExecuteMessageParams + { + /// + /// URI for the editor that owns the query + /// + public string OwnerUri { get; set; } + + /// + /// The message that is being returned + /// + public ResultMessage Message { get; set; } + } + + public class QueryExecuteMessageEvent + { + public static readonly + EventType Type = + EventType.Create("query/message"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs index 7630b712..b5671fd9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs @@ -28,10 +28,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public class QueryExecuteResult { - /// - /// Informational messages from the query runner. Optional, can be set to null. - /// - public string Messages { get; set; } } public class QueryExecuteRequest diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs index 90f66015..cc7ffc10 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs @@ -12,6 +12,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public class ResultMessage { + /// + /// ID of the batch that generated this message. If null, this message + /// was not generated as part of a batch + /// + public int? BatchId { get; set; } + + /// + /// Whether or not this message is an error + /// + public bool IsError { get; set; } + /// /// Timestamp of the message /// Stored in UTC ISO 8601 format; should be localized before displaying to any user @@ -23,20 +34,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string Message { get; set; } - /// - /// Full constructor - /// - public ResultMessage(string timeStamp, string message) - { - Time = timeStamp; - Message = message; - } - /// /// Constructor with default "Now" time /// - public ResultMessage(string message) + public ResultMessage(string message, bool isError, int? batchId) { + BatchId = batchId; + IsError = isError; Time = DateTime.Now.ToString("o"); Message = message; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index d5948a8a..1f23bc0b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -99,6 +99,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public event Batch.BatchAsyncEventHandler BatchCompleted; + /// + /// Event that will be called when a message has been emitted + /// + public event Batch.BatchAsyncMessageHandler BatchMessageSent; + /// /// Event to be called when a batch starts execution. /// @@ -272,6 +277,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Don't actually execute if there aren't any batches to execute if (Batches.Length == 0) { + if (BatchMessageSent != null) + { + await BatchMessageSent(new ResultMessage(SR.QueryServiceCompletedSuccessfully, false, null)); + } + if (QueryCompleted != null) + { + await QueryCompleted(this); + } return; } @@ -308,6 +321,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { b.BatchStart += BatchStarted; b.BatchCompletion += BatchCompleted; + b.BatchMessageSent += BatchMessageSent; b.ResultSetCompletion += ResultSetCompleted; await b.Execute(conn, cancellationSource.Token); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 7f307fee..b23e07eb 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -370,10 +370,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } // Send the result stating that the query was successfully started - await requestContext.SendResult(new QueryExecuteResult - { - Messages = newQuery.Batches.Length == 0 ? SR.QueryServiceCompletedSuccessfully : null - }); + await requestContext.SendResult(new QueryExecuteResult()); return newQuery; } @@ -411,7 +408,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams { OwnerUri = executeParams.OwnerUri, - Message = errorMessage + //Message = errorMessage }; await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); }; @@ -443,6 +440,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution }; query.BatchCompleted += batchCompleteCallback; + Batch.BatchAsyncMessageHandler batchMessageCallback = async m => + { + QueryExecuteMessageParams eventParams = new QueryExecuteMessageParams + { + Message = m, + OwnerUri = executeParams.OwnerUri + }; + await requestContext.SendEvent(QueryExecuteMessageEvent.Type, eventParams); + }; + query.BatchMessageSent += batchMessageCallback; + // Setup the ResultSet completion callback ResultSet.ResultSetAsyncEventHandler resultCallback = async r => { diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Connection/ReliableConnectionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Connection/ReliableConnectionTests.cs index 4a1f7f3c..49ac3e47 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Connection/ReliableConnectionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Connection/ReliableConnectionTests.cs @@ -777,9 +777,6 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Connection var detectionStrategy2 = new TestSqlAzureTemporaryAndIgnorableErrorDetectionStrategy(); Assert.NotNull(detectionStrategy2.InvokeCanRetrySqlException(sqlException)); Assert.NotNull(detectionStrategy2.InvokeShouldIgnoreSqlException(sqlException)); - - Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, Common.GetFileStreamFactory(null)); - batch.UnwrapDbException(sqlException); } var unknownCodeReason = RetryPolicy.ThrottlingReason.FromReasonCode(-1); diff --git a/test/Microsoft.SqlTools.ServiceLayer.PerfTests/Tests/QueryExecutionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.PerfTests/Tests/QueryExecutionTests.cs index 8d41cd1e..e5758458 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.PerfTests/Tests/QueryExecutionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.PerfTests/Tests/QueryExecutionTests.cs @@ -79,7 +79,7 @@ namespace Microsoft.SqlTools.ServiceLayer.PerfTests }; var result = await testHelper.Driver.SendRequest(QueryExecuteRequest.Type, queryParams); - if (result != null && string.IsNullOrEmpty(result.Messages)) + if (result != null) { TestTimer timer = new TestTimer() { PrintResult = true }; await Common.ExecuteWithTimeout(timer, 100000, async () => diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 4a5ead4b..f70c529c 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -97,6 +97,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return output; } + public static Dictionary[][] GetTestDataSet(int dataSets) + { + List[]> output = new List[]>(); + for(int dataSet = 0; dataSet < dataSets; dataSet++) + { + output.Add(StandardTestData); + } + return output.ToArray(); + } + public static async Task AwaitExecution(QueryExecutionService service, QueryExecuteParams qeParams, RequestContext requestContext) { diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/BatchTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/BatchTests.cs index 9ac1f6c2..64133fa0 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/BatchTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/BatchTests.cs @@ -7,8 +7,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; @@ -32,12 +31,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... It should not have executed and no error Assert.False(batch.HasExecuted, "The query should not have executed."); - Assert.False(batch.HasError, "The batch should not have an error"); // ... The results should be empty Assert.Empty(batch.ResultSets); Assert.Empty(batch.ResultSummaries); - Assert.Empty(batch.ResultMessages); // ... The start line of the batch should be 0 Assert.Equal(0, batch.Selection.StartLine); @@ -46,318 +43,186 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution Assert.Equal(Common.Ordinal, batch.Id); // ... The summary should have the same info - Assert.False(batch.Summary.HasError); Assert.Equal(Common.Ordinal, batch.Summary.Id); Assert.Null(batch.Summary.ResultSetSummaries); - Assert.Null(batch.Summary.Messages); Assert.Equal(0, batch.Summary.Selection.StartLine); Assert.NotEqual(default(DateTime).ToString("o"), batch.Summary.ExecutionStart); // Should have been set at construction Assert.Null(batch.Summary.ExecutionEnd); Assert.Null(batch.Summary.ExecutionElapsed); } - /// - /// Note: This test also tests the start notification feature - /// [Fact] - public void BatchExecuteNoResultSets() + public async Task BatchExecuteNoResultSets() { // Setup: - // ... Create a callback for batch start - BatchSummary batchSummaryFromStart = null; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchSummaryFromStart = b.Summary; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion - BatchSummary batchSummaryFromCompletion = null; - Batch.BatchAsyncEventHandler batchCompleteCallback = b => - { - batchSummaryFromCompletion = b.Summary; - return Task.FromResult(0); - }; - - // ... Create a callback for result completion - bool resultCallbackFired = false; - ResultSet.ResultSetAsyncEventHandler resultSetCallback = r => - { - resultCallbackFired = true; - return Task.FromResult(0); - }; + // ... Keep track of callbacks being called + int batchStartCalls = 0; + int batchEndCalls = 0; + int resultSetCalls = 0; + List messages = new List(); // If I execute a query that should get no result sets var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); - batch.BatchStart += batchStartCallback; - batch.BatchCompletion += batchCompleteCallback; - batch.ResultSetCompletion += resultSetCallback; - batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait(); + BatchCallbackHelper(batch, + b => batchStartCalls++, + b => batchEndCalls++, + m => messages.Add(m), + r => resultSetCalls++); + await batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None); // Then: - // ... It should have executed without error - Assert.True(batch.HasExecuted, "The query should have been marked executed."); - Assert.False(batch.HasError, "The batch should not have an error"); + // ... Callbacks should have been called the appropriate number of times + Assert.Equal(1, batchStartCalls); + Assert.Equal(1, batchEndCalls); + Assert.Equal(0, resultSetCalls); - // ... The results should be empty - Assert.Empty(batch.ResultSets); - Assert.Empty(batch.ResultSummaries); - - // ... The results should not be null - Assert.NotNull(batch.ResultSets); - Assert.NotNull(batch.ResultSummaries); - - // ... There should be a message for how many rows were affected - Assert.Equal(1, batch.ResultMessages.Count()); - - // ... The callback for batch start should have been called - // ... The info from it should have been basic - Assert.NotNull(batchSummaryFromStart); - Assert.False(batchSummaryFromStart.HasError); - Assert.Equal(Common.Ordinal, batchSummaryFromStart.Id); - Assert.Equal(Common.SubsectionDocument, batchSummaryFromStart.Selection); - Assert.True(DateTime.Parse(batchSummaryFromStart.ExecutionStart) > default(DateTime)); - Assert.Null(batchSummaryFromStart.ResultSetSummaries); - Assert.Null(batchSummaryFromStart.Messages); - Assert.Null(batchSummaryFromStart.ExecutionElapsed); - Assert.Null(batchSummaryFromStart.ExecutionEnd); - - // ... The callback for batch completion should have been fired - // ... The summary should match the expected info - Assert.NotNull(batchSummaryFromCompletion); - Assert.False(batchSummaryFromCompletion.HasError); - Assert.Equal(Common.Ordinal, batchSummaryFromCompletion.Id); - Assert.Equal(0, batchSummaryFromCompletion.ResultSetSummaries.Length); - Assert.Equal(1, batchSummaryFromCompletion.Messages.Length); - Assert.Equal(Common.SubsectionDocument, batchSummaryFromCompletion.Selection); - Assert.True(DateTime.Parse(batchSummaryFromCompletion.ExecutionStart) > default(DateTime)); - Assert.True(DateTime.Parse(batchSummaryFromCompletion.ExecutionEnd) > default(DateTime)); - Assert.NotNull(batchSummaryFromCompletion.ExecutionElapsed); - - // ... The callback for the result set should NOT have been fired - Assert.False(resultCallbackFired); + // ... The batch and the summary should be correctly assigned + ValidateBatch(batch, 0); + ValidateBatchSummary(batch); + ValidateMessages(batch, 1, messages); } [Fact] - public void BatchExecuteOneResultSet() + public async Task BatchExecuteOneResultSet() { + // Setup: + // ... Keep track of callbacks being called + int batchStartCalls = 0; + int batchEndCalls = 0; + int resultSetCalls = 0; + List messages = new List(); + + // ... Build a data set to return const int resultSets = 1; - ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); - - // Setup: Create a callback for batch completion - BatchSummary batchSummaryFromCallback = null; - Batch.BatchAsyncEventHandler batchCallback = b => - { - batchSummaryFromCallback = b.Summary; - return Task.FromResult(0); - }; - - // ... Create a callback for result set completion - bool resultCallbackFired = false; - ResultSet.ResultSetAsyncEventHandler resultSetCallback = r => - { - resultCallbackFired = true; - return Task.FromResult(0); - }; + ConnectionInfo ci = Common.CreateTestConnectionInfo(Common.GetTestDataSet(resultSets), false); // If I execute a query that should get one result set var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); - batch.BatchCompletion += batchCallback; - batch.ResultSetCompletion += resultSetCallback; - batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); + BatchCallbackHelper(batch, + b => batchStartCalls++, + b => batchEndCalls++, + m => messages.Add(m), + r => resultSetCalls++); + await batch.Execute(GetConnection(ci), CancellationToken.None); // Then: - // ... It should have executed without error - Assert.True(batch.HasExecuted, "The batch should have been marked executed."); - Assert.False(batch.HasError, "The batch should not have an error"); + // ... Callbacks should have been called the appropriate number of times + Assert.Equal(1, batchStartCalls); + Assert.Equal(1, batchEndCalls); + Assert.Equal(1, resultSetCalls); // ... There should be exactly one result set - Assert.Equal(resultSets, batch.ResultSets.Count); - Assert.Equal(resultSets, batch.ResultSummaries.Length); - - // ... Inside the result set should be with 5 rows - Assert.Equal(Common.StandardRows, batch.ResultSets.First().RowCount); - Assert.Equal(Common.StandardRows, batch.ResultSummaries[0].RowCount); - - // ... Inside the result set should have 5 columns - Assert.Equal(Common.StandardColumns, batch.ResultSets.First().Columns.Length); - Assert.Equal(Common.StandardColumns, batch.ResultSummaries[0].ColumnInfo.Length); - - // ... There should be a message for how many rows were affected - Assert.Equal(resultSets, batch.ResultMessages.Count()); - - // ... The callback for batch completion should have been fired - Assert.NotNull(batchSummaryFromCallback); - - // ... The callback for resultset completion should have been fired - Assert.True(resultCallbackFired); // We only want to validate that it happened, validation of the - // summary is done in result set tests + ValidateBatch(batch, resultSets); + ValidateBatchSummary(batch); + ValidateMessages(batch, 1, messages); } [Fact] - public void BatchExecuteTwoResultSets() + public async Task BatchExecuteTwoResultSets() { - var dataset = new[] { Common.StandardTestData, Common.StandardTestData }; - int resultSets = dataset.Length; - ConnectionInfo ci = Common.CreateTestConnectionInfo(dataset, false); + // Setup: + // ... Keep track of callbacks being called + int batchStartCalls = 0; + int batchEndCalls = 0; + int resultSetCalls = 0; + List messages = new List(); - // Setup: Create a callback for batch completion - BatchSummary batchSummaryFromCallback = null; - Batch.BatchAsyncEventHandler batchCallback = b => - { - batchSummaryFromCallback = b.Summary; - return Task.FromResult(0); - }; - - // ... Create a callback for resultset completion - int resultSummaryCount = 0; - ResultSet.ResultSetAsyncEventHandler resultSetCallback = r => - { - resultSummaryCount++; - return Task.FromResult(0); - }; + // ... Build a data set to return + const int resultSets = 2; + ConnectionInfo ci = Common.CreateTestConnectionInfo(Common.GetTestDataSet(resultSets), false); // If I execute a query that should get two result sets var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); - batch.BatchCompletion += batchCallback; - batch.ResultSetCompletion += resultSetCallback; - batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); + BatchCallbackHelper(batch, + b => batchStartCalls++, + b => batchEndCalls++, + m => messages.Add(m), + r => resultSetCalls++); + await batch.Execute(GetConnection(ci), CancellationToken.None); // Then: + // ... Callbacks should have been called the appropriate number of times + Assert.Equal(1, batchStartCalls); + Assert.Equal(1, batchEndCalls); + Assert.Equal(2, resultSetCalls); + // ... It should have executed without error - Assert.True(batch.HasExecuted, "The batch should have been marked executed."); - Assert.False(batch.HasError, "The batch should not have an error"); - - // ... There should be exactly two result sets - Assert.Equal(resultSets, batch.ResultSets.Count()); - - foreach (ResultSet rs in batch.ResultSets) - { - // ... Each result set should have 5 rows - Assert.Equal(Common.StandardRows, rs.RowCount); - - // ... Inside each result set should be 5 columns - Assert.Equal(Common.StandardColumns, rs.Columns.Length); - } - - // ... There should be exactly two result set summaries - Assert.Equal(resultSets, batch.ResultSummaries.Length); - - foreach (ResultSetSummary rs in batch.ResultSummaries) - { - // ... Inside each result summary, there should be 5 rows - Assert.Equal(Common.StandardRows, rs.RowCount); - - // ... Inside each result summary, there should be 5 column definitions - Assert.Equal(Common.StandardColumns, rs.ColumnInfo.Length); - } - - // ... The callback for batch completion should have been fired - Assert.NotNull(batchSummaryFromCallback); - - // ... The callback for result set completion should have been fired - Assert.Equal(2, resultSummaryCount); + ValidateBatch(batch, resultSets); + ValidateBatchSummary(batch); + ValidateMessages(batch, 1, messages); } [Fact] - public void BatchExecuteInvalidQuery() + public async Task BatchExecuteInvalidQuery() { - // Setup: - // ... Create a callback for batch start - bool batchStartCalled = false; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchStartCalled = true; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion - BatchSummary batchSummaryFromCallback = null; - Batch.BatchAsyncEventHandler batchCompleteCallback = b => - { - batchSummaryFromCallback = b.Summary; - return Task.FromResult(0); - }; - - // ... Create a callback that will fail the test if it's called - ResultSet.ResultSetAsyncEventHandler resultSetCallback = r => - { - throw new Exception("ResultSet callback was called when it should not have been."); - }; - - ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); + // Setup: + // ... Keep track of callbacks being called + int batchStartCalls = 0; + int batchEndCalls = 0; + List messages = new List(); // If I execute a batch that is invalid + var ci = Common.CreateTestConnectionInfo(null, true); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); - batch.BatchStart += batchStartCallback; - batch.BatchCompletion += batchCompleteCallback; - batch.ResultSetCompletion += resultSetCallback; - batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); + BatchCallbackHelper(batch, + b => batchStartCalls++, + b => batchEndCalls++, + m => messages.Add(m), + r => { throw new Exception("ResultSet callback was called when it should not have been."); }); + await batch.Execute(GetConnection(ci), CancellationToken.None); // Then: - // ... It should have executed with error - Assert.True(batch.HasExecuted); - Assert.True(batch.HasError); + // ... Callbacks should have been called the appropriate number of times + Assert.Equal(1, batchStartCalls); + Assert.Equal(1, batchEndCalls); - // ... There should be no result sets - Assert.Empty(batch.ResultSets); - Assert.Empty(batch.ResultSummaries); + // ... It should have executed without error + ValidateBatch(batch, 0); + ValidateBatchSummary(batch); - // ... There should be plenty of messages for the error - Assert.NotEmpty(batch.ResultMessages); - - // ... The callback for batch completion should have been fired - Assert.NotNull(batchSummaryFromCallback); - - // ... The callback for batch start should have been fired - Assert.True(batchStartCalled); + // ... There should be one error message returned + Assert.Equal(1, messages.Count); + Assert.All(messages, m => + { + Assert.True(m.IsError); + Assert.Equal(batch.Id, m.BatchId); + }); } [Fact] public async Task BatchExecuteExecuted() { - ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); + // Setup: Build a data set to return + const int resultSets = 1; + ConnectionInfo ci = Common.CreateTestConnectionInfo(Common.GetTestDataSet(resultSets), false); // If I execute a batch var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); - batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); + await batch.Execute(GetConnection(ci), CancellationToken.None); // Then: // ... It should have executed without error Assert.True(batch.HasExecuted, "The batch should have been marked executed."); - Assert.False(batch.HasError, "The batch should not have an error"); - - // Setup for part 2: - // ... Create a callback for batch completion - Batch.BatchAsyncEventHandler completeCallback = b => - { - throw new Exception("Batch completion callback should not have been called"); - }; - - // ... Create a callback for batch start - Batch.BatchAsyncEventHandler startCallback = b => - { - throw new Exception("Batch start callback should not have been called"); - }; // If I execute it again // Then: // ... It should throw an invalid operation exception - batch.BatchStart += startCallback; - batch.BatchCompletion += completeCallback; - await Assert.ThrowsAsync(() => - batch.Execute(GetConnection(ci), CancellationToken.None)); + BatchCallbackHelper(batch, + b => { throw new Exception("Batch start callback should not have been called"); }, + b => { throw new Exception("Batch completion callback should not have been called"); }, + m => { throw new Exception("Message callback should not have been called"); }, + null); + await Assert.ThrowsAsync( + () => batch.Execute(GetConnection(ci), CancellationToken.None)); // ... The data should still be available without error - Assert.False(batch.HasError, "The batch should not be in an error condition"); - Assert.True(batch.HasExecuted, "The batch should still be marked executed."); - Assert.NotEmpty(batch.ResultSets); - Assert.NotEmpty(batch.ResultSummaries); + ValidateBatch(batch, resultSets); + ValidateBatchSummary(batch); } [Theory] @@ -397,13 +262,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution { // If: // ... I call the StatementCompletedHandler + Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, Common.GetFileStreamFactory(null)); + int messageCalls = 0; + batch.BatchMessageSent += args => + { + messageCalls++; + return Task.FromResult(0); + }; + // Then: - // ... a ResultMaessage should be logged in the resultsMessages collection - Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, Common.GetFileStreamFactory(null)); - batch.StatementCompletedHandler(null, new StatementCompletedEventArgs(1)); - Assert.True(batch.ResultMessages.Count() == 1); - batch.StatementCompletedHandler(null, new StatementCompletedEventArgs(2)); - Assert.True(batch.ResultMessages.Count() == 2); + // ... The message handler for the batch should havve been called twice + batch.StatementCompletedHandler(null, new StatementCompletedEventArgs(1)); + Assert.True(messageCalls == 1); + batch.StatementCompletedHandler(null, new StatementCompletedEventArgs(2)); + Assert.True(messageCalls == 2); } private static DbConnection GetConnection(ConnectionInfo info) @@ -411,5 +283,81 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution return info.Factory.CreateSqlConnection(ConnectionService.BuildConnectionString(info.ConnectionDetails)); } + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + private static void ValidateBatch(Batch batch, int expectedResultSets) + { + // The batch should be executed + Assert.True(batch.HasExecuted, "The query should have been marked executed."); + + // Result set list should never be null + Assert.NotNull(batch.ResultSets); + Assert.NotNull(batch.ResultSummaries); + + // Make sure the number of result sets matches + Assert.Equal(expectedResultSets, batch.ResultSets.Count); + Assert.Equal(expectedResultSets, batch.ResultSummaries.Length); + } + + private static void ValidateBatchSummary(Batch batch) + { + BatchSummary batchSummary = batch.Summary; + + Assert.NotNull(batchSummary); + Assert.Equal(batch.Id, batchSummary.Id); + Assert.Equal(batch.ResultSets.Count, batchSummary.ResultSetSummaries.Length); + Assert.Equal(batch.Selection, batchSummary.Selection); + + // Something other than default date is provided for start and end times + Assert.True(DateTime.Parse(batchSummary.ExecutionStart) > default(DateTime)); + Assert.True(DateTime.Parse(batchSummary.ExecutionEnd) > default(DateTime)); + Assert.NotNull(batchSummary.ExecutionElapsed); + } + + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + private static void ValidateMessages(Batch batch, int expectedMessages, IList messages) + { + // There should be equal number of messages to result sets + Assert.Equal(expectedMessages, messages.Count); + + // No messages should be errors + // All messages must have the batch ID + Assert.All(messages, m => + { + Assert.False(m.IsError); + Assert.Equal(batch.Id, m.BatchId); + }); + } + + private static void BatchCallbackHelper(Batch batch, Action startCallback, Action endCallback, + Action messageCallback, Action resultCallback) + { + // Setup the callback for batch start + batch.BatchStart += b => + { + startCallback?.Invoke(b); + return Task.FromResult(0); + }; + + // Setup the callback for batch completion + batch.BatchCompletion += b => + { + endCallback?.Invoke(b); + return Task.FromResult(0); + }; + + // Setup the callback for batch messages + batch.BatchMessageSent += (m) => + { + messageCallback?.Invoke(m); + return Task.FromResult(0); + }; + + // Setup the result set completion callback + batch.ResultSetCompletion += r => + { + resultCallback?.Invoke(r); + return Task.FromResult(0); + }; + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/QueryTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/QueryTests.cs index 3df09e13..d2a67ede 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/QueryTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/QueryTests.cs @@ -5,9 +5,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Castle.Components.DictionaryAdapter; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Xunit; @@ -16,6 +19,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution public class QueryTests { + [Fact] + public void QueryCreationCorrect() + { + // If: + // ... I create a query + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); + Query query = new Query(Common.StandardQuery, ci, new QueryExecutionSettings(), fileStreamFactory); + + // Then: + // ... I should get back two batches to execute that haven't been executed + Assert.NotEmpty(query.QueryText); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + } + [Fact] public void QueryExecuteNoQueryText() { @@ -63,44 +82,30 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution public void QueryExecuteSingleBatch() { // Setup: - // ... Create a callback for atch start + // ... Keep track of how many times the callbacks were called int batchStartCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchStartCallbacksReceived++; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion int batchCompleteCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchCompleteCallback = summary => - { - batchCompleteCallbacksReceived++; - return Task.CompletedTask; - }; + int batchMessageCallbacksReceived = 0; // If: // ... I create a query from a single batch (without separator) ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Query query = new Query(Common.StandardQuery, ci, new QueryExecutionSettings(), fileStreamFactory); - query.BatchStarted += batchStartCallback; - query.BatchCompleted += batchCompleteCallback; + BatchCallbackHelper(query, + b => batchStartCallbacksReceived++, + b => batchCompleteCallbacksReceived++, + m => batchMessageCallbacksReceived++); - // Then: - // ... I should get a single batch to execute that hasn't been executed - Assert.NotEmpty(query.QueryText); - Assert.NotEmpty(query.Batches); - Assert.Equal(1, query.Batches.Length); - Assert.False(query.HasExecuted); - Assert.Throws(() => query.BatchSummaries); - - // If: // ... I then execute the query query.Execute(); query.ExecutionTask.Wait(); // Then: + // ... There should be exactly 1 batch + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + // ... The query should have completed successfully with one batch summary returned Assert.True(query.HasExecuted); Assert.NotEmpty(query.BatchSummaries); @@ -109,38 +114,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... The batch callbacks should have been called precisely 1 time Assert.Equal(1, batchStartCallbacksReceived); Assert.Equal(1, batchCompleteCallbacksReceived); + Assert.Equal(1, batchMessageCallbacksReceived); } [Fact] - public void QueryExecuteNoOpBatch() + public void QueryExecuteSingleNoOpBatch() { - // Setup: - // ... Create a callback for batch startup - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - throw new Exception("Batch startup callback should not have been called."); - }; - - // ... Create a callback for batch completion - Batch.BatchAsyncEventHandler batchCompletionCallback = summary => - { - throw new Exception("Batch completion callback was called"); - }; + // Setup: Keep track of all the messages received + List messages = new List(); // If: // ... I create a query from a single batch that does nothing ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Query query = new Query(Common.NoOpQuery, ci, new QueryExecutionSettings(), fileStreamFactory); - query.BatchStarted += batchStartCallback; - query.BatchCompleted += batchCompletionCallback; - - // Then: - // ... I should get no batches back - Assert.NotEmpty(query.QueryText); - Assert.Empty(query.Batches); - Assert.False(query.HasExecuted); - Assert.Throws(() => query.BatchSummaries); + BatchCallbackHelper(query, + b => { throw new Exception("Batch startup callback should not have been called."); }, + b => { throw new Exception("Batch completion callback was called"); }, + m => messages.Add(m)); // If: // ... I Then execute the query @@ -148,30 +139,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution query.ExecutionTask.Wait(); // Then: + // ... There should be no batches + Assert.Empty(query.Batches); + // ... The query should have completed successfully with no batch summaries returned Assert.True(query.HasExecuted); Assert.Empty(query.BatchSummaries); + + // ... The message callback should have been called exactly once + // ... The message must not have a batch associated with it + Assert.Equal(1, messages.Count); + Assert.Null(messages[0].BatchId); } [Fact] - public void QueryExecuteMultipleBatches() + public void QueryExecuteMultipleResultBatches() { // Setup: - // ... Create a callback for batch start + // ... Keep track of how many callbacks are received int batchStartCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchStartCallbacksReceived++; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion int batchCompletedCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchCompletedCallback = summary => - { - batchCompletedCallbacksReceived++; - return Task.FromResult(0); - }; + int batchMessageCallbacksReceived = 0; // If: // ... I create a query from two batches (with separator) @@ -179,52 +167,39 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution string queryText = string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory); - query.BatchStarted += batchStartCallback; - query.BatchCompleted += batchCompletedCallback; + BatchCallbackHelper(query, + b => batchStartCallbacksReceived++, + b => batchCompletedCallbacksReceived++, + m => batchMessageCallbacksReceived++); - // Then: - // ... I should get back two batches to execute that haven't been executed - Assert.NotEmpty(query.QueryText); - Assert.NotEmpty(query.Batches); - Assert.Equal(2, query.Batches.Length); - Assert.False(query.HasExecuted); - Assert.Throws(() => query.BatchSummaries); - - // If: // ... I then execute the query query.Execute(); query.ExecutionTask.Wait(); // Then: + // ... I should get back a query with one batch (no op batch is not included) + Assert.NotEmpty(query.Batches); + Assert.Equal(2, query.Batches.Length); + // ... The query should have completed successfully with two batch summaries returned Assert.True(query.HasExecuted); Assert.NotEmpty(query.BatchSummaries); Assert.Equal(2, query.BatchSummaries.Length); - // ... The batch start and completion callbacks should have been called precisely 2 times + // ... The batch start, complete, and message callbacks should have been called precisely 2 times Assert.Equal(2, batchStartCallbacksReceived); Assert.Equal(2, batchCompletedCallbacksReceived); + Assert.Equal(2, batchMessageCallbacksReceived); } [Fact] public void QueryExecuteMultipleBatchesWithNoOp() { // Setup: - // ... Create a callback for batch start + // ... Keep track of how many times callbacks are called int batchStartCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchStartCallbacksReceived++; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion int batchCompletionCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchCompletionCallback = summary => - { - batchCompletionCallbacksReceived++; - return Task.CompletedTask; - }; + int batchMessageCallbacksReceived = 0; // If: // ... I create a query from a two batches (with separator) @@ -232,22 +207,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution string queryText = string.Format("{0}\r\nGO\r\n{1}", Common.StandardQuery, Common.NoOpQuery); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory); - query.BatchStarted += batchStartCallback; - query.BatchCompleted += batchCompletionCallback; + BatchCallbackHelper(query, + b => batchStartCallbacksReceived++, + b => batchCompletionCallbacksReceived++, + m => batchMessageCallbacksReceived++); - // Then: - // ... I should get back one batch to execute that hasn't been executed - Assert.NotEmpty(query.QueryText); - Assert.NotEmpty(query.Batches); - Assert.Equal(1, query.Batches.Length); - Assert.False(query.HasExecuted); - Assert.Throws(() => query.BatchSummaries); - - // If: // .. I then execute the query query.Execute(); query.ExecutionTask.Wait(); + // Then: + // ... I should get back a query with one batch (no op batch is not included) + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + // ... The query should have completed successfully with one batch summary returned Assert.True(query.HasExecuted); Assert.NotEmpty(query.BatchSummaries); @@ -256,61 +229,107 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... The batch callbacks should have been called precisely 1 time Assert.Equal(1, batchStartCallbacksReceived); Assert.Equal(1, batchCompletionCallbacksReceived); + Assert.Equal(1, batchMessageCallbacksReceived); + } + + [Fact] + public async Task QueryExecuteMultipleNoOpBatches() + { + // Setup: + // ... Keep track of how many messages were sent + List messages = new List(); + + // If: + // ... I create a query from a two batches (with separator) + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + string queryText = string.Format("{0}\r\nGO\r\n{1}", Common.NoOpQuery, Common.NoOpQuery); + var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); + Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory); + BatchCallbackHelper(query, + b => { throw new Exception("Batch start handler was called"); }, + b => { throw new Exception("Batch completed handler was called"); }, + m => messages.Add(m)); + + // .. I then execute the query + query.Execute(); + await query.ExecutionTask; + + // Then: + // ... I should get back a query with no batches + Assert.Empty(query.Batches); + + // ... The query should have completed successfully with one zero batch summaries returned + Assert.True(query.HasExecuted); + Assert.Empty(query.BatchSummaries); + + // ... The message callback should have been called exactly once + // ... The message must not have a batch associated with it + Assert.Equal(1, messages.Count); + Assert.Null(messages[0].BatchId); } [Fact] public void QueryExecuteInvalidBatch() { // Setup: - // ... Create a callback for batch start + // ... Keep track of how many times a method is called int batchStartCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchStartCallback = b => - { - batchStartCallbacksReceived++; - return Task.FromResult(0); - }; - - // ... Create a callback for batch completion int batchCompletionCallbacksReceived = 0; - Batch.BatchAsyncEventHandler batchCompltionCallback = summary => - { - batchCompletionCallbacksReceived++; - return Task.CompletedTask; - }; + List messages = new List(); // If: // ... I create a query from an invalid batch ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary()); Query query = new Query(Common.InvalidQuery, ci, new QueryExecutionSettings(), fileStreamFactory); - query.BatchStarted += batchStartCallback; - query.BatchCompleted += batchCompltionCallback; + BatchCallbackHelper(query, + b => batchStartCallbacksReceived++, + b => batchCompletionCallbacksReceived++, + m => messages.Add(m)); - // Then: - // ... I should get back a query with one batch not executed - Assert.NotEmpty(query.QueryText); - Assert.NotEmpty(query.Batches); - Assert.Equal(1, query.Batches.Length); - Assert.False(query.HasExecuted); - Assert.Throws(() => query.BatchSummaries); - - // If: // ... I then execute the query query.Execute(); query.ExecutionTask.Wait(); // Then: + // ... I should get back a query with one batch + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + // ... There should be an error on the batch Assert.True(query.HasExecuted); Assert.NotEmpty(query.BatchSummaries); Assert.Equal(1, query.BatchSummaries.Length); - Assert.True(query.BatchSummaries[0].HasError); - Assert.NotEmpty(query.BatchSummaries[0].Messages); + Assert.True(messages.Any(m => m.IsError)); // ... The batch callbacks should have been called once Assert.Equal(1, batchStartCallbacksReceived); Assert.Equal(1, batchCompletionCallbacksReceived); } + private static void BatchCallbackHelper(Query q, Action startCallback, Action endCallback, + Action messageCallback) + { + // Setup the callback for batch start + q.BatchStarted += b => + { + startCallback?.Invoke(b); + return Task.FromResult(0); + }; + + // Setup the callback for batch completion + q.BatchCompleted += b => + { + endCallback?.Invoke(b); + return Task.FromResult(0); + }; + + // Setup the callback for batch messages + q.BatchMessageSent += (m) => + { + messageCallback?.Invoke(m); + return Task.FromResult(0); + }; + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/ServiceIntegrationTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/ServiceIntegrationTests.cs index ecea14ab..975f16a0 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/ServiceIntegrationTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/ServiceIntegrationTests.cs @@ -21,14 +21,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a valid query with all batches as no op var workspaceService = GetDefaultWorkspaceService(string.Format("{0}\r\nGO\r\n{0}", Common.NoOpQuery)); var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); - var queryParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri }; + var queryParams = new QueryExecuteParams {QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri}; var efv = new EventFlowValidator() - .AddResultValidation(p => + .AddStandardQueryResultValidator() + .AddStandardMessageValidator() + .AddEventValidation(QueryExecuteCompleteEvent.Type, p => { - Assert.False(string.IsNullOrWhiteSpace(p.Messages)); - }) - .Complete(); + // Validate OwnerURI matches + Assert.Equal(Common.OwnerUri, p.OwnerUri); + Assert.NotNull(p.BatchSummaries); + Assert.Equal(0, p.BatchSummaries.Length); + }).Complete(); await Common.AwaitExecution(queryService, queryParams, efv.Object); // Then: @@ -38,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... There should be one active query Assert.Equal(1, queryService.ActiveQueries.Count); } - + [Fact] public async Task QueryExecuteSingleBatchNoResultsTest() { @@ -46,11 +50,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a valid query with no results var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); - var queryParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri }; + var queryParams = new QueryExecuteParams {QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri}; var efv = new EventFlowValidator() .AddStandardQueryResultValidator() .AddStandardBatchStartValidator() + .AddStandardMessageValidator() .AddStandardBatchCompleteValidator() .AddStandardQueryCompleteValidator(1) .Complete(); @@ -71,18 +76,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // If: // ... I request to execute a valid query with results var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); - var queryService = Common.GetPrimedExecutionService(new[] { Common.StandardTestData }, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryService = Common.GetPrimedExecutionService(new[] {Common.StandardTestData}, true, false, + workspaceService); + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; var efv = new EventFlowValidator() .AddStandardQueryResultValidator() .AddStandardBatchStartValidator() .AddStandardResultSetValidator() + .AddStandardMessageValidator() .AddStandardBatchCompleteValidator() .AddStandardQueryCompleteValidator(1) .Complete(); await Common.AwaitExecution(queryService, queryParams, efv.Object); - + // Then: // ... All events should have been called as per their flow validator efv.Validate(); @@ -97,15 +104,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // If: // ... I request to execute a valid query with one batch and multiple result sets var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); - var dataset = new[] { Common.StandardTestData, Common.StandardTestData }; + var dataset = new[] {Common.StandardTestData, Common.StandardTestData}; var queryService = Common.GetPrimedExecutionService(dataset, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; var efv = new EventFlowValidator() .AddStandardQueryResultValidator() .AddStandardBatchStartValidator() .AddStandardResultSetValidator() .AddStandardResultSetValidator() + .AddStandardMessageValidator() .AddStandardQueryCompleteValidator(1) .Complete(); await Common.AwaitExecution(queryService, queryParams, efv.Object); @@ -124,17 +132,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // If: // ... I request a to execute a valid query with multiple batches var workspaceService = GetDefaultWorkspaceService(string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery)); - var dataSet = new[] { Common.StandardTestData }; + var dataSet = new[] {Common.StandardTestData}; var queryService = Common.GetPrimedExecutionService(dataSet, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; var efv = new EventFlowValidator() .AddStandardQueryResultValidator() .AddStandardBatchStartValidator() .AddStandardResultSetValidator() + .AddStandardMessageValidator() .AddStandardBatchCompleteValidator() .AddStandardBatchCompleteValidator() .AddStandardResultSetValidator() + .AddStandardMessageValidator() .AddStandardBatchCompleteValidator() .AddStandardQueryCompleteValidator(2) .Complete(); @@ -156,7 +166,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a query using a file URI that isn't connected var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); var queryService = Common.GetPrimedExecutionService(null, false, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = "notConnected", QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = "notConnected", QuerySelection = Common.WholeDocument}; var efv = new EventFlowValidator() .AddErrorValidation(Assert.NotEmpty) @@ -178,14 +188,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a query var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; // Note, we don't care about the results of the first request var firstRequestContext = RequestContextMocks.Create(null); await Common.AwaitExecution(queryService, queryParams, firstRequestContext.Object); // ... And then I request another query without waiting for the first to complete - queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished var efv = new EventFlowValidator() .AddErrorValidation(Assert.NotEmpty) .Complete(); @@ -206,7 +216,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a query var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; // Note, we don't care about the results of the first request var firstRequestContext = RequestContextMocks.Create(null); @@ -240,7 +250,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // If: // ... I request to execute a query with a missing query string var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = null }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = null}; var efv = new EventFlowValidator() .AddErrorValidation(Assert.NotEmpty) @@ -262,7 +272,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution // ... I request to execute a query that is invalid var workspaceService = GetDefaultWorkspaceService(Common.StandardQuery); var queryService = Common.GetPrimedExecutionService(null, true, true, workspaceService); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument }; + var queryParams = new QueryExecuteParams {OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument}; var efv = new EventFlowValidator() .AddStandardQueryResultValidator() @@ -270,7 +280,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution .AddStandardBatchCompleteValidator() .AddStandardQueryCompleteValidator(1) .Complete(); - await Common.AwaitExecution(queryService, queryParams, efv.Object); // Then: @@ -295,10 +304,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution this EventFlowValidator efv) { // We just need to makes sure we get a result back, there's no params to validate - return efv.AddResultValidation(r => - { - Assert.Null(r.Messages); - }); + return efv.AddResultValidation(Assert.NotNull); } public static EventFlowValidator AddStandardBatchStartValidator( @@ -317,18 +323,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution { return efv.AddEventValidation(QueryExecuteBatchCompleteEvent.Type, p => { - // Validate OwnerURI and batch summary are returned + // Validate OwnerURI and result summary are returned Assert.Equal(Common.OwnerUri, p.OwnerUri); Assert.NotNull(p.BatchSummary); }); } + public static EventFlowValidator AddStandardMessageValidator( + this EventFlowValidator efv) + { + return efv.AddEventValidation(QueryExecuteMessageEvent.Type, p => + { + // Validate OwnerURI and message are returned + Assert.Equal(Common.OwnerUri, p.OwnerUri); + Assert.NotNull(p.Message); + }); + } + public static EventFlowValidator AddStandardResultSetValidator( this EventFlowValidator efv) { return efv.AddEventValidation(QueryExecuteResultSetCompleteEvent.Type, p => { - // Validate OwnerURI and result summary are returned + // Validate OwnerURI and summary are returned Assert.Equal(Common.OwnerUri, p.OwnerUri); Assert.NotNull(p.ResultSetSummary); }); @@ -339,7 +356,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution { return efv.AddEventValidation(QueryExecuteCompleteEvent.Type, p => { - Assert.True(string.IsNullOrWhiteSpace(p.Message)); Assert.Equal(Common.OwnerUri, p.OwnerUri); Assert.NotNull(p.BatchSummaries); Assert.Equal(expectedBatches, p.BatchSummaries.Length); diff --git a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/QueryExecutionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/QueryExecutionTests.cs index 75df010c..a9eb9ae7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/QueryExecutionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/QueryExecutionTests.cs @@ -327,34 +327,33 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests } */ - [Fact] - public async Task NoOpQueryReturnsMessage() + [Theory] + [InlineData("-- no-op")] + [InlineData("GO")] + [InlineData("GO -- no-op")] + public async Task NoOpQueryReturnsMessage(string query) { - // Given queries that do nothing (no-ops)... - var queries = new string[] - { - "-- no-op", - "GO", - "GO -- no-op" - }; - using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) using (TestHelper testHelper = new TestHelper()) { - foreach (var query in queries) - { - Assert.True(await testHelper.Connect(queryTempFile.FilePath, ConnectionTestUtils.LocalhostConnection)); + Assert.True(await testHelper.Connect(queryTempFile.FilePath, ConnectionTestUtils.LocalhostConnection)); - // If the queries are executed... - var queryResult = await testHelper.RunQueryAsync(queryTempFile.FilePath, query); + // If: the query is executed... + var queryResult = await testHelper.RunQueryAsync(queryTempFile.FilePath, query); + var message = await testHelper.WaitForMessage(); - // Then I expect messages that the commands were completed successfully to be in the result - Assert.NotNull(queryResult); - Assert.NotNull(queryResult.Messages); - Assert.Equal("Commands completed successfully.", queryResult.Messages); + // Then: + // ... I expect a query result to indicate successfully started query + Assert.NotNull(queryResult); - await testHelper.Disconnect(queryTempFile.FilePath); - } + // ... I expect a non-error message to be returned without a batch associated with it + Assert.NotNull(message); + Assert.NotNull(message.Message); + Assert.NotNull(message.Message.Message); + Assert.False(message.Message.IsError); + Assert.Null(message.Message.BatchId); + + await testHelper.Disconnect(queryTempFile.FilePath); } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/TestHelper.cs b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/TestHelper.cs index d248ffff..255f54a6 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/TestHelper.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/TestHelper.cs @@ -295,7 +295,7 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests }; var result = await Driver.SendRequest(QueryExecuteRequest.Type, queryParams); - if (result != null && string.IsNullOrEmpty(result.Messages)) + if (result != null) { var eventResult = await Driver.WaitForEvent(QueryExecuteCompleteEvent.Type, timeoutMilliseconds); return eventResult; @@ -384,6 +384,15 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests return result; } + /// + /// Waits for a message to be returned by the service + /// + /// A message from the service layer + public async Task WaitForMessage() + { + return await Driver.WaitForEvent(QueryExecuteMessageEvent.Type); + } + public void WriteToFile(string ownerUri, string query) { lock (fileLock)