diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 831e981b..eeaa8903 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -42,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public Query(string queryText, ConnectionInfo connection) { // Sanity check for input - if (queryText == null) + if (String.IsNullOrWhiteSpace(queryText)) { throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); } @@ -68,50 +68,55 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } // Create a connection from the connection details - string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); - using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) + try { - await conn.OpenAsync(cancellationSource.Token); - - // Create a command that we'll use for executing the query - using (DbCommand command = conn.CreateCommand()) + string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); + using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) { - command.CommandText = QueryText; - command.CommandType = CommandType.Text; + await conn.OpenAsync(cancellationSource.Token); - // Execute the command to get back a reader - using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token)) + // Create a command that we'll use for executing the query + using (DbCommand command = conn.CreateCommand()) { - do + command.CommandText = QueryText; + command.CommandType = CommandType.Text; + + // Execute the command to get back a reader + using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token)) { - // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows - if (!reader.HasRows) + do { - continue; - } + // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows + if (!reader.HasRows) + { + continue; + } - // Read until we hit the end of the result set - ResultSet resultSet = new ResultSet(); - while (await reader.ReadAsync(cancellationSource.Token)) - { - resultSet.AddRow(reader); - } + // Read until we hit the end of the result set + ResultSet resultSet = new ResultSet(); + while (await reader.ReadAsync(cancellationSource.Token)) + { + resultSet.AddRow(reader); + } - // Read off the column schema information - if (reader.CanGetColumnSchema()) - { - resultSet.Columns = reader.GetColumnSchema().ToArray(); - } + // Read off the column schema information + if (reader.CanGetColumnSchema()) + { + resultSet.Columns = reader.GetColumnSchema().ToArray(); + } - // Add the result set to the results of the query - ResultSets.Add(resultSet); - } while (await reader.NextResultAsync(cancellationSource.Token)); + // Add the result set to the results of the query + ResultSets.Add(resultSet); + } while (await reader.NextResultAsync(cancellationSource.Token)); + } } } } - - // Mark that we have executed - HasExecuted = true; + finally + { + // Mark that we have executed + HasExecuted = true; + } } public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 540390ee..b98b5ac8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -1,5 +1,11 @@ -using System; +// +// 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.Concurrent; +using System.Data.Common; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; @@ -36,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution private readonly Lazy> queries = new Lazy>(() => new ConcurrentDictionary()); - private ConcurrentDictionary ActiveQueries + internal ConcurrentDictionary ActiveQueries { get { return queries.Value; } } @@ -68,59 +74,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public async Task HandleExecuteRequest(QueryExecuteParams executeParams, RequestContext requestContext) { - // Attempt to get the connection for the editor - ConnectionInfo connectionInfo; - if(!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + try { - await requestContext.SendError("This editor is not connected to a database."); - return; + // Get a query new active query + Query newQuery = await CreateAndActivateNewQuery(executeParams, requestContext); + + // Execute the query + await ExecuteAndCompleteQuery(executeParams, requestContext, newQuery); } - - // If there is already an in-flight query, error out - Query newQuery = new Query(executeParams.QueryText, connectionInfo); - if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) + catch (Exception e) { - await requestContext.SendError("A query is already in progress for this editor session." + - "Please cancel this query or wait for its completion."); - return; + // Dump any unexpected exceptions as errors + await requestContext.SendError(e.Message); } - - // Launch the query and respond with successfully launching it - Task executeTask = newQuery.Execute(); - await requestContext.SendResult(new QueryExecuteResult - { - Messages = null - }); - - // Wait for query execution and then send back the results - await Task.WhenAll(executeTask); - QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams - { - Error = false, - Messages = new string[]{}, // TODO: Figure out how to get messages back from the server - OwnerUri = executeParams.OwnerUri, - ResultSetSummaries = newQuery.ResultSummary - }; - await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); } public async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, RequestContext requestContext) { - // Attempt to load the query - Query query; - if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) - { - var errorResult = new QueryExecuteSubsetResult - { - Message = "The requested query does not exist." - }; - await requestContext.SendResult(errorResult); - return; - } - try { + // Attempt to load the query + Query query; + if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) + { + await requestContext.SendResult(new QueryExecuteSubsetResult + { + Message = "The requested query does not exist." + }); + return; + } + // Retrieve the requested subset and return it var result = new QueryExecuteSubsetResult { @@ -130,34 +114,143 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution }; await requestContext.SendResult(result); } - catch (Exception e) + catch (InvalidOperationException ioe) { + // Return the error as a result await requestContext.SendResult(new QueryExecuteSubsetResult { - Message = e.Message + Message = ioe.Message }); } + catch (ArgumentOutOfRangeException aoore) + { + // Return the error as a result + await requestContext.SendResult(new QueryExecuteSubsetResult + { + Message = aoore.Message + }); + } + catch (Exception e) + { + // This was unexpected, so send back as error + await requestContext.SendError(e.Message); + } } public async Task HandleDisposeRequest(QueryDisposeParams disposeParams, RequestContext requestContext) { - // Attempt to remove the query for the owner uri - Query result; - if (!ActiveQueries.TryRemove(disposeParams.OwnerUri, out result)) + try { - await requestContext.SendError("Failed to dispose query, ID not found."); - return; - } + // Attempt to remove the query for the owner uri + Query result; + if (!ActiveQueries.TryRemove(disposeParams.OwnerUri, out result)) + { + await requestContext.SendResult(new QueryDisposeResult + { + Messages = "Failed to dispose query, ID not found." + }); + return; + } - // Success - await requestContext.SendResult(new QueryDisposeResult + // Success + await requestContext.SendResult(new QueryDisposeResult + { + Messages = null + }); + } + catch (Exception e) { - Messages = null - }); + await requestContext.SendError(e.Message); + } } #endregion + private async Task CreateAndActivateNewQuery(QueryExecuteParams executeParams, RequestContext requestContext) + { + try + { + // Attempt to get the connection for the editor + ConnectionInfo connectionInfo; + if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + { + await requestContext.SendResult(new QueryExecuteResult + { + Messages = "This editor is not connected to a database." + }); + return null; + } + + // Attempt to clean out any old query on the owner URI + Query oldQuery; + if (ActiveQueries.TryGetValue(executeParams.OwnerUri, out oldQuery) && oldQuery.HasExecuted) + { + ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery); + } + + // If we can't add the query now, it's assumed the query is in progress + Query newQuery = new Query(executeParams.QueryText, connectionInfo); + if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) + { + await requestContext.SendResult(new QueryExecuteResult + { + Messages = "A query is already in progress for this editor session." + + "Please cancel this query or wait for its completion." + }); + return null; + } + + return newQuery; + } + catch (ArgumentNullException ane) + { + await requestContext.SendResult(new QueryExecuteResult { Messages = ane.Message }); + return null; + } + // Any other exceptions will fall through here and be collected at the end + } + + private async Task ExecuteAndCompleteQuery(QueryExecuteParams executeParams, RequestContext requestContext, Query query) + { + // Skip processing if the query is null + if (query == null) + { + return; + } + + // Launch the query and respond with successfully launching it + Task executeTask = query.Execute(); + await requestContext.SendResult(new QueryExecuteResult + { + Messages = null + }); + + try + { + // Wait for query execution and then send back the results + await Task.WhenAll(executeTask); + QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams + { + Error = false, + Messages = new string[] { }, // TODO: Figure out how to get messages back from the server + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = query.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); + } + catch (DbException dbe) + { + // Dump the message to a complete event + QueryExecuteCompleteParams errorEvent = new QueryExecuteCompleteParams + { + Error = true, + Messages = new[] {dbe.Message}, + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = query.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, errorEvent); + } + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 144f3526..9bc8053b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -2,9 +2,13 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Moq; using Moq.Protected; @@ -13,6 +17,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class Common { + public const string OwnerUri = "testFile"; + public static readonly Dictionary[] StandardTestData = { new Dictionary { {"col1", "val11"}, { "col2", "val12"}, { "col3", "val13"}, { "col4", "col14"} }, @@ -45,43 +51,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return query; } - #region Mocking - - //private static DbDataReader CreateTestReader(int columnCount, int rowCount) - //{ - // var readerMock = new Mock { CallBase = true }; - - // // Setup for column reads - // // TODO: We can't test columns because of oddities with how datatable/GetColumn - - // // Setup for row reads - // var readSequence = readerMock.SetupSequence(dbReader => dbReader.Read()); - // for (int i = 0; i < rowCount; i++) - // { - // readSequence.Returns(true); - // } - // readSequence.Returns(false); - - // // Make sure that if we call for data from the reader it works - // readerMock.Setup(dbReader => dbReader[InColumnRange(columnCount)]) - // .Returns(i => i.ToString()); - // readerMock.Setup(dbReader => dbReader[NotInColumnRange(columnCount)]) - // .Throws(new ArgumentOutOfRangeException()); - // readerMock.Setup(dbReader => dbReader.HasRows) - // .Returns(rowCount > 0); - - // return readerMock.Object; - //} - - //private static int InColumnRange(int columnCount) - //{ - // return Match.Create(i => i < columnCount && i > 0); - //} - - //private static int NotInColumnRange(int columnCount) - //{ - // return Match.Create(i => i >= columnCount || i < 0); - //} + #region DbConnection Mocking public static DbCommand CreateTestCommand(Dictionary[][] data, bool throwOnRead) { @@ -138,5 +108,74 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution #endregion + #region Service Mocking + + public static ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails + { + DatabaseName = "123", + Password = "456", + ServerName = "789", + UserName = "012" + }; + } + + public static QueryExecutionService GetPrimedExecutionService(ISqlConnectionFactory factory, bool isConnected) + { + var connectionService = new ConnectionService(factory); + if (isConnected) + { + connectionService.Connect(new ConnectParams + { + Connection = GetTestConnectionDetails(), + OwnerUri = OwnerUri + }); + } + return new QueryExecutionService(connectionService); + } + + #endregion + + #region Request Mocking + + public static Mock> GetQueryExecuteResultContextMock( + Action resultCallback, + Action, QueryExecuteCompleteParams> eventCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendEvent + var sendEventFlow = requestContext.Setup(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny())) + .Returns(Task.FromResult(0)); + if (eventCallback != null) + { + sendEventFlow.Callback(eventCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + #endregion } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs index def3c6b6..c0fed697 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs @@ -1,11 +1,93 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Moq; +using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class DisposeTests { + [Fact] + public void DisposeExecutedQuery() + { + // If: + // ... I request a query (doesn't matter what kind) + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams {QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri}; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... And then I dispose of the query + var disposeParams = new QueryDisposeParams {OwnerUri = Common.OwnerUri}; + QueryDisposeResult result = null; + var disposeRequest = GetQueryDisposeResultContextMock(qdr => result = qdr, null); + queryService.HandleDisposeRequest(disposeParams, disposeRequest.Object).Wait(); + + // Then: + // ... I should have seen a successful result + // ... And the active queries should be empty + VerifyQueryDisposeCallCount(disposeRequest, Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(queryService.ActiveQueries); + } + + [Fact] + public void QueryDisposeMissingQuery() + { + // If: + // ... I attempt to dispose a query that doesn't exist + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), false); + var disposeParams = new QueryDisposeParams {OwnerUri = Common.OwnerUri}; + QueryDisposeResult result = null; + var disposeRequest = GetQueryDisposeResultContextMock(qdr => result = qdr, null); + queryService.HandleDisposeRequest(disposeParams, disposeRequest.Object).Wait(); + + // Then: + // ... I should have gotten an error result + VerifyQueryDisposeCallCount(disposeRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + } + + #region Mocking + + private Mock> GetQueryDisposeResultContextMock( + Action resultCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext + .Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + private void VerifyQueryDisposeCallCount(Mock> mock, Times sendResultCalls, + Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } + + #endregion + } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index b9eebfd4..ffc0a870 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,13 +1,18 @@ using System; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Moq; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class ExecuteTests { + #region Query Class Tests + [Fact] public void QueryCreationTest() { @@ -160,5 +165,222 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.NotEmpty(query.ResultSets); Assert.NotEmpty(query.ResultSummary); } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void QueryExecuteNoQuery(string query) + { + // If: + // ... I create a query that has an empty query + // Then: + // ... It should throw an exception + Assert.Throws(() => new Query(query, null)); + } + + [Fact] + public void QueryExecuteNoConnectionInfo() + { + // If: + // ... I create a query that has a null connection info + // Then: + // ... It should throw an exception + Assert.Throws(() => new Query("Some Query", null)); + } + + #endregion + + #region Service Tests + + [Fact] + public void QueryExecuteValidNoResultsTest() + { + // If: + // ... I request to execute a valid query with no results + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var queryParams = new QueryExecuteParams { QueryText = "Doesn't Matter", OwnerUri = Common.OwnerUri }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No Errors should have been sent + // ... A successful result should have been sent with no messages + // ... A completion event should have been fired with empty results + // ... There should be one active query + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(completeParams.Messages); + Assert.Empty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [Fact] + public void QueryExecuteValidResultsTest() + { + // If: + // ... I request to execute a valid query with results + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Doesn't Matter" }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A successful result should have been sent with no messages + // ... A completion event should have been fired with one result + // ... There should be one active query + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(completeParams.Messages); + Assert.NotEmpty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [Fact] + public void QueryExecuteUnconnectedUriTest() + { + // If: + // ... I request to execute a query using a file URI that isn't connected + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), false); + var queryParams = new QueryExecuteParams { OwnerUri = "notConnected", QueryText = "Doesn't Matter" }; + + QueryExecuteResult result = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... An error message should have been returned via the result + // ... No completion event should have been fired + // ... No error event should have been fired + // ... There should be no active queries + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + Assert.Empty(queryService.ActiveQueries); + } + + [Fact] + public void QueryExecuteInProgressTest() + { + // If: + // ... I request to execute a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Some Query" }; + + // Note, we don't care about the results of the first request + var firstRequestContext = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(queryParams, firstRequestContext.Object).Wait(); + + // ... And then I request another query without waiting for the first to complete + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished + QueryExecuteResult result = null; + var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A result should have been sent with an error message + // ... No completion event should have been fired + // ... There should only be one active query + VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.AtMostOnce(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [Fact] + public void QueryExecuteCompletedTest() + { + // If: + // ... I request to execute a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Some Query" }; + + // Note, we don't care about the results of the first request + var firstRequestContext = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(queryParams, firstRequestContext.Object).Wait(); + + // ... And then I request another query after waiting for the first to complete + QueryExecuteResult result = null; + QueryExecuteCompleteParams complete = null; + var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, qecp) => complete = qecp, null); + queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A result should have been sent with no errors + // ... There should only be one active query + VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.False(complete.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void QueryExecuteMissingQueryTest(string query) + { + // If: + // ... I request to execute a query with a missing query string + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = query }; + + QueryExecuteResult result = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A result should have been sent with an error message + // ... No completion event should have been fired + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + } + + [Fact] + public void QueryExecuteInvalidQueryTest() + { + // If: + // ... I request to execute a query that is invalid + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, true), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Bad query!" }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams complete = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, qecp) => complete = qecp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A result should have been sent with success (we successfully started the query) + // ... A completion event should have been sent with error + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.True(complete.Error); + Assert.NotEmpty(complete.Messages); + } + + #endregion + + private void VerifyQueryExecuteCallCount(Mock> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny()), sendEventCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs deleted file mode 100644 index 58e5800f..00000000 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; -using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; -using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; -using Microsoft.SqlTools.ServiceLayer.QueryExecution; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.Test.Utility; -using Moq; -using Xunit; - -namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution -{ - public class ServiceTests - { - - [Fact] - public void QueryExecuteValidNoResultsTest() - { - // If: - // ... I request to execute a valid query with no results - var queryService = GetPrimedExecutionService(Common.CreateMockFactory(null, false)); - var queryParams = new QueryExecuteParams - { - QueryText = "Doesn't Matter", - OwnerUri = "testFile" - }; - - QueryExecuteResult result = null; - QueryExecuteCompleteParams completeParams = null; - var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); - queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); - - // Then: - // ... No Errors should have been sent - // ... A successful result should have been sent with no messages - // ... A completion event should have been fired with empty results - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.Empty(completeParams.Messages); - Assert.Empty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); - } - - [Fact] - public void QueryExecuteValidResultsTest() - { - // If: - // ... I request to execute a valid query with results - var queryService = GetPrimedExecutionService(Common.CreateMockFactory(new [] {Common.StandardTestData}, false)); - var queryParams = new QueryExecuteParams {OwnerUri = "testFile", QueryText = "Doesn't Matter"}; - - QueryExecuteResult result = null; - QueryExecuteCompleteParams completeParams = null; - var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); - queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); - - // Then: - // ... No errors should have been send - // ... A successful result should have been sent with no messages - // ... A completion event should hvae been fired with one result - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.Empty(completeParams.Messages); - Assert.NotEmpty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); - } - - - - private ConnectionDetails GetTestConnectionDetails() - { - return new ConnectionDetails - { - DatabaseName = "123", - Password = "456", - ServerName = "789", - UserName = "012" - }; - } - - private QueryExecutionService GetPrimedExecutionService(ISqlConnectionFactory factory) - { - var connectionService = new ConnectionService(factory); - connectionService.Connect(new ConnectParams {Connection = GetTestConnectionDetails(), OwnerUri = "testFile"}); - return new QueryExecutionService(connectionService); - } - - private Mock> GetQueryExecuteResultContextMock( - Action resultCallback, - Action, QueryExecuteCompleteParams> eventCallback, - Action errorCallback) - { - var requestContext = new Mock>(); - - // Setup the mock for SendResult - var sendResultFlow = requestContext - .Setup(rc => rc.SendResult(It.IsAny())) - .Returns(Task.FromResult(0)); - if (resultCallback != null) - { - sendResultFlow.Callback(resultCallback); - } - - // Setup the mock for SendEvent - var sendEventFlow = requestContext.Setup(rc => rc.SendEvent( - It.Is>(m => m == QueryExecuteCompleteEvent.Type), - It.IsAny())) - .Returns(Task.FromResult(0)); - if (eventCallback != null) - { - sendEventFlow.Callback(eventCallback); - } - - // Setup the mock for SendError - var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny())) - .Returns(Task.FromResult(0)); - if (errorCallback != null) - { - sendErrorFlow.Callback(errorCallback); - } - - return requestContext; - } - - private void VerifyQueryExecuteCallCount(Mock> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls) - { - mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); - mock.Verify(rc => rc.SendEvent( - It.Is>(m => m == QueryExecuteCompleteEvent.Type), - It.IsAny()), sendEventCalls); - mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); - } - } -} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index c89b643f..bdb0dc48 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -1,12 +1,17 @@ using System; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Moq; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class SubsetTests { + #region Query Class Tests + [Theory] [InlineData(2)] [InlineData(20)] @@ -53,5 +58,149 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... It should throw an exception Assert.Throws(() => q.GetSubset(resultSetIndex, rowStartInex, rowCount)); } + + #endregion + + #region Service Intergration Tests + + [Fact] + public void SubsetServiceValidTest() + { + // If: + // ... I have a query that has results (doesn't matter what) + var queryService =Common.GetPrimedExecutionService( + Common.CreateMockFactory(new[] {Common.StandardTestData}, false), true); + var executeParams = new QueryExecuteParams {QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri}; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... And I then ask for a valid set of results from it + var subsetParams = new QueryExecuteSubsetParams {OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0}; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should have a successful result + // ... There should be rows there (other test validate that the rows are correct) + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.Null(result.Message); + Assert.NotNull(result.ResultSubset); + } + + [Fact] + public void SubsetServiceMissingQueryTest() + { + // If: + // ... I ask for a set of results for a file that hasn't executed a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should have an error result + // ... There should be no rows in the result set + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + [Fact] + public void SubsetServiceUnexecutedQueryTest() + { + // If: + // ... I have a query that hasn't finished executing (doesn't matter what) + var queryService = Common.GetPrimedExecutionService( + Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true); + var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri }; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; + + // ... And I then ask for a valid set of results from it + var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should get an error result + // ... There should not be rows + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + [Fact] + public void SubsetServiceOutOfRangeSubsetTest() + { + // If: + // ... I have a query that doesn't have any result sets + var queryService = Common.GetPrimedExecutionService( + Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri }; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... And I then ask for a set of results from it + var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should get an error result + // ... There should not be rows + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + #endregion + + #region Mocking + + private Mock> GetQuerySubsetResultContextMock( + Action resultCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext + .Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + private void VerifyQuerySubsetCallCount(Mock> mock, Times sendResultCalls, + Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } + + #endregion + } }