diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs index 616fc2c6..a390eae2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs @@ -80,13 +80,36 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } } + + private ConnectionService connectionService = null; + + /// + /// Internal for testing purposes only + /// + internal ConnectionService ConnectionServiceInstance + { + get + { + if(connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } + public void InitializeService(ServiceHost serviceHost) { // Register a callback for when a connection is created - ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); + ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache); // Register a callback for when a connection is closed - ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); + ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); } private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo) @@ -97,6 +120,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } + /// + /// Intellisense cache count access for testing. + /// + internal int GetCacheCount() + { + return caches.Count; + } + /// /// Remove a reference to an autocomplete cache from a URI. If /// it is the last URI connected to a particular connection, @@ -157,7 +188,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // that are not backed by a SQL connection ConnectionInfo info; IntellisenseCache cache; - if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out info) + if (ConnectionServiceInstance.TryFindConnection(textDocumentPosition.Uri, out info) && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache)) { return cache.GetAutoCompleteItems(textDocumentPosition).ToArray(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index 80ea3ec9..ddf82059 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -3,11 +3,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +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.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.Test.Utility; +using Moq; +using Moq.Protected; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices @@ -19,6 +26,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices { #region "Diagnostics tests" + /// + /// Verify that the latest SqlParser (2016 as of this writing) is used by default + /// + [Fact] + public void LatestSqlParserIsUsedByDefault() + { + // This should only parse correctly on SQL server 2016 or newer + const string sql2016Text = + @"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" + + @"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" + + @"ON [dbo].[Customer];"; + + LanguageService service = TestObjects.GetTestLanguageService(); + + // parse + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sql2016Text); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify that no errors are detected + Assert.Equal(0, fileMarkers.Length); + } + /// /// Verify that the SQL parser correctly detects errors in text /// @@ -108,24 +138,179 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices #region "Autocomplete Tests" /// - /// Verify that the SQL parser correctly detects errors in text + /// Creates a mock db command that returns a predefined result set + /// + public static DbCommand CreateTestCommand(Dictionary[][] data) + { + var commandMock = new Mock { CallBase = true }; + var commandMockSetup = commandMock.Protected() + .Setup("ExecuteDbDataReader", It.IsAny()); + + commandMockSetup.Returns(new TestDbDataReader(data)); + + return commandMock.Object; + } + + /// + /// Creates a mock db connection that returns predefined data when queried for a result set + /// + public DbConnection CreateMockDbConnection(Dictionary[][] data) + { + var connectionMock = new Mock { CallBase = true }; + connectionMock.Protected() + .Setup("CreateDbCommand") + .Returns(CreateTestCommand(data)); + + return connectionMock.Object; + } + + /// + /// Verify that the autocomplete service returns tables for the current connection as suggestions /// [Fact] - public async Task AutocompleteTest() + public void TablesAreReturnedAsAutocompleteSuggestions() { - // TODO Re-enable this test once we have a way to hook up the right auto-complete and connection services. - // Probably need a service provider channel so that we can mock service access. Otherwise everything accesses - // static instances and cannot be properly tested. - - //var autocompleteService = TestObjects.GetAutoCompleteService(); - //var connectionService = TestObjects.GetTestConnectionService(); + // Result set for the query of database tables + Dictionary[] data = + { + new Dictionary { {"name", "master" } }, + new Dictionary { {"name", "model" } } + }; - //ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); - //var connectionResult = connectionService.Connect(connectionRequest); + var mockFactory = new Mock(); + mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) + .Returns(CreateMockDbConnection(new[] {data})); + + var connectionService = TestObjects.GetTestConnectionService(); + var autocompleteService = new AutoCompleteService(); + autocompleteService.ConnectionServiceInstance = connectionService; + autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); + + autocompleteService.ConnectionFactory = mockFactory.Object; - //var sqlConnection = connectionService.ActiveConnections[connectionResult.ConnectionId]; - //await autocompleteService.UpdateAutoCompleteCache(sqlConnection); - await Task.Run(() => { return; }); + // Open a connection + // The cache should get updated as part of this + ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); + var connectionResult = connectionService.Connect(connectionRequest); + Assert.NotEmpty(connectionResult.ConnectionId); + + // Check that there is one cache created in the auto complete service + Assert.Equal(1, autocompleteService.GetCacheCount()); + + // Check that we get table suggestions for an autocomplete request + TextDocumentPosition position = new TextDocumentPosition(); + position.Uri = connectionRequest.OwnerUri; + position.Position = new Position(); + position.Position.Line = 1; + position.Position.Character = 1; + var items = autocompleteService.GetCompletionItems(position); + Assert.Equal(2, items.Length); + Assert.Equal("master", items[0].Label); + Assert.Equal("model", items[1].Label); + } + + /// + /// Verify that only one intellisense cache is created for two documents using + /// the autocomplete service when they share a common connection. + /// + [Fact] + public void OnlyOneCacheIsCreatedForTwoDocumentsWithSameConnection() + { + var connectionService = TestObjects.GetTestConnectionService(); + var autocompleteService = new AutoCompleteService(); + autocompleteService.ConnectionServiceInstance = connectionService; + autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); + + // Open two connections + ConnectParams connectionRequest1 = TestObjects.GetTestConnectionParams(); + connectionRequest1.OwnerUri = "file:///my/first/file.sql"; + ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams(); + connectionRequest2.OwnerUri = "file:///my/second/file.sql"; + var connectionResult1 = connectionService.Connect(connectionRequest1); + Assert.NotEmpty(connectionResult1.ConnectionId); + var connectionResult2 = connectionService.Connect(connectionRequest2); + Assert.NotEmpty(connectionResult2.ConnectionId); + + // Verify that only one intellisense cache is created to service both URI's + Assert.Equal(1, autocompleteService.GetCacheCount()); + } + + /// + /// Verify that two different intellisense caches and corresponding autocomplete + /// suggestions are provided for two documents with different connections. + /// + [Fact] + public void TwoCachesAreCreatedForTwoDocumentsWithDifferentConnections() + { + // Result set for the query of database tables + Dictionary[] data1 = + { + new Dictionary { {"name", "master" } }, + new Dictionary { {"name", "model" } } + }; + + Dictionary[] data2 = + { + new Dictionary { {"name", "master" } }, + new Dictionary { {"name", "my_table" } }, + new Dictionary { {"name", "my_other_table" } } + }; + + var mockFactory = new Mock(); + mockFactory.SetupSequence(factory => factory.CreateSqlConnection(It.IsAny())) + .Returns(CreateMockDbConnection(new[] {data1})) + .Returns(CreateMockDbConnection(new[] {data2})); + + var connectionService = TestObjects.GetTestConnectionService(); + var autocompleteService = new AutoCompleteService(); + autocompleteService.ConnectionServiceInstance = connectionService; + autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); + + autocompleteService.ConnectionFactory = mockFactory.Object; + + // Open connections + // The cache should get updated as part of this + ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); + connectionRequest.OwnerUri = "file:///my/first/sql/file.sql"; + var connectionResult = connectionService.Connect(connectionRequest); + Assert.NotEmpty(connectionResult.ConnectionId); + + // Check that there is one cache created in the auto complete service + Assert.Equal(1, autocompleteService.GetCacheCount()); + + // Open second connection + ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams(); + connectionRequest2.OwnerUri = "file:///my/second/sql/file.sql"; + connectionRequest2.Connection.DatabaseName = "my_other_db"; + var connectionResult2 = connectionService.Connect(connectionRequest2); + Assert.NotEmpty(connectionResult2.ConnectionId); + + // Check that there are now two caches in the auto complete service + Assert.Equal(2, autocompleteService.GetCacheCount()); + + // Check that we get 2 different table suggestions for autocomplete requests + TextDocumentPosition position = new TextDocumentPosition(); + position.Uri = connectionRequest.OwnerUri; + position.Position = new Position(); + position.Position.Line = 1; + position.Position.Character = 1; + + var items = autocompleteService.GetCompletionItems(position); + Assert.Equal(2, items.Length); + Assert.Equal("master", items[0].Label); + Assert.Equal("model", items[1].Label); + + TextDocumentPosition position2 = new TextDocumentPosition(); + position2.Uri = connectionRequest2.OwnerUri; + position2.Position = new Position(); + position2.Position.Line = 1; + position2.Position.Character = 1; + + var items2 = autocompleteService.GetCompletionItems(position2); + Assert.Equal(3, items2.Length); + Assert.Equal("master", items2[0].Label); + Assert.Equal("my_table", items2[1].Label); + Assert.Equal("my_other_table", items2[2].Label); } #endregion