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