From 53e26798fc28cfa55dfe4836238c16f3c346ac46 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 25 Jul 2016 13:04:14 -0700 Subject: [PATCH] Language Service diagnostics and autocomplete (#9) * Merge master to dev (#4) * Misc. clean-ups related to removing unneeded PowerShell Language Service code. * Remove unneeded files and clean up remaining code. * Enable file change tracking with Workspace and EditorSession. * Setup standard src, test folder structure. Add unit test project. * Actually stage the deletes. Update .gitignore * Integrate SqlParser into the onchange diagnostics to provide error messages. * Add tests for the language service diagnostics * Initial implementation for autocomplete. * Switch to using sys.tables for autocomplete Move some code into a better class * Delete unused csproj file. * Add nuget.config to pickup SQL Parser nuget package --- nuget.config | 15 ++ .../Connection/ConnectionMessages.cs | 63 +++++++ .../Connection/ConnectionService.cs | 160 ++++++++++++++++++ src/ServiceHost/Connection/ISqlConnection.cs | 34 ++++ src/ServiceHost/Connection/SqlConnection.cs | 72 ++++++++ .../LanguageSupport/AutoCompleteService.cs | 112 ++++++++++++ .../LanguageSupport/LanguageService.cs | 57 +++++-- src/ServiceHost/Server/LanguageServer.cs | 94 ++++++++-- src/ServiceHost/Workspace/ScriptFile.cs | 21 ++- src/ServiceHost/project.json | 5 +- .../Connection/ConnectionServiceTests.cs | 60 +++++++ .../LanguageServer/LanguageServiceTests.cs | 126 ++++++++++++++ test/ServiceHost.Test/Utility/TestObjects.cs | 108 ++++++++++++ test/ServiceHost.Test/project.json | 8 +- 14 files changed, 894 insertions(+), 41 deletions(-) create mode 100644 nuget.config create mode 100644 src/ServiceHost/Connection/ConnectionMessages.cs create mode 100644 src/ServiceHost/Connection/ConnectionService.cs create mode 100644 src/ServiceHost/Connection/ISqlConnection.cs create mode 100644 src/ServiceHost/Connection/SqlConnection.cs create mode 100644 src/ServiceHost/LanguageSupport/AutoCompleteService.cs create mode 100644 test/ServiceHost.Test/Connection/ConnectionServiceTests.cs create mode 100644 test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs create mode 100644 test/ServiceHost.Test/Utility/TestObjects.cs diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..33539216 --- /dev/null +++ b/nuget.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/ServiceHost/Connection/ConnectionMessages.cs b/src/ServiceHost/Connection/ConnectionMessages.cs new file mode 100644 index 00000000..814e55f0 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionMessages.cs @@ -0,0 +1,63 @@ +// +// 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.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Message format for the initial connection request + /// + public class ConnectionDetails + { + /// + /// Gets or sets the connection server name + /// + public string ServerName { get; set; } + + /// + /// Gets or sets the connection database name + /// + public string DatabaseName { get; set; } + + /// + /// Gets or sets the connection user name + /// + public string UserName { get; set; } + + /// + /// Gets or sets the connection password + /// + /// + public string Password { get; set; } + } + + /// + /// Message format for the connection result response + /// + public class ConnectionResult + { + /// + /// Gets or sets the connection id + /// + public int ConnectionId { get; set; } + + /// + /// Gets or sets any connection error messages + /// + public string Messages { get; set; } + } + + /// + /// Connect request mapping entry + /// + public class ConnectionRequest + { + public static readonly + RequestType Type = + RequestType.Create("connection/connect"); + } + +} diff --git a/src/ServiceHost/Connection/ConnectionService.cs b/src/ServiceHost/Connection/ConnectionService.cs new file mode 100644 index 00000000..8f0634a7 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionService.cs @@ -0,0 +1,160 @@ +// +// 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.Generic; +using System.Data.SqlClient; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Main class for the Connection Management services + /// + public class ConnectionService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new ConnectionService()); + + /// + /// The SQL connection factory object + /// + private ISqlConnectionFactory connectionFactory; + + /// + /// The current connection id that was previously used + /// + private int maxConnectionId = 0; + + /// + /// Active connections lazy dictionary instance + /// + private Lazy> activeConnections + = new Lazy>(() + => new Dictionary()); + + /// + /// Callback for onconnection handler + /// + /// + public delegate Task OnConnectionHandler(ISqlConnection sqlConnection); + + /// + /// List of onconnection handlers + /// + private readonly List onConnectionActivities = new List(); + + /// + /// Gets the active connection map + /// + public Dictionary ActiveConnections + { + get + { + return activeConnections.Value; + } + } + + /// + /// Gets the singleton service instance + /// + public static ConnectionService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Gets the SQL connection factory instance + /// + public ISqlConnectionFactory ConnectionFactory + { + get + { + if (this.connectionFactory == null) + { + this.connectionFactory = new SqlConnectionFactory(); + } + return this.connectionFactory; + } + } + + /// + /// Default constructor is private since it's a singleton class + /// + private ConnectionService() + { + } + + /// + /// Test constructor that injects dependency interfaces + /// + /// + public ConnectionService(ISqlConnectionFactory testFactory) + { + this.connectionFactory = testFactory; + } + + /// + /// Open a connection with the specified connection details + /// + /// + public ConnectionResult Connect(ConnectionDetails connectionDetails) + { + // build the connection string from the input parameters + string connectionString = BuildConnectionString(connectionDetails); + + // create a sql connection instance + ISqlConnection connection = this.ConnectionFactory.CreateSqlConnection(); + + // open the database + connection.OpenDatabaseConnection(connectionString); + + // map the connection id to the connection object for future lookups + this.ActiveConnections.Add(++maxConnectionId, connection); + + // invoke callback notifications + foreach (var activity in this.onConnectionActivities) + { + activity(connection); + } + + // return the connection result + return new ConnectionResult() + { + ConnectionId = maxConnectionId + }; + } + + /// + /// Add a new method to be called when the onconnection request is submitted + /// + /// + public void RegisterOnConnectionTask(OnConnectionHandler activity) + { + onConnectionActivities.Add(activity); + } + + /// + /// Build a connection string from a connection details instance + /// + /// + private string BuildConnectionString(ConnectionDetails connectionDetails) + { + SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); + connectionBuilder["Data Source"] = connectionDetails.ServerName; + connectionBuilder["Integrated Security"] = false; + connectionBuilder["User Id"] = connectionDetails.UserName; + connectionBuilder["Password"] = connectionDetails.Password; + connectionBuilder["Initial Catalog"] = connectionDetails.DatabaseName; + return connectionBuilder.ToString(); + } + } +} diff --git a/src/ServiceHost/Connection/ISqlConnection.cs b/src/ServiceHost/Connection/ISqlConnection.cs new file mode 100644 index 00000000..4ef80c5e --- /dev/null +++ b/src/ServiceHost/Connection/ISqlConnection.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Interface for the SQL Connection factory + /// + public interface ISqlConnectionFactory + { + /// + /// Create a new SQL Connection object + /// + ISqlConnection CreateSqlConnection(); + } + + /// + /// Interface for the SQL Connection wrapper + /// + public interface ISqlConnection + { + /// + /// Open a connection to the provided connection string + /// + /// + void OpenDatabaseConnection(string connectionString); + + IEnumerable GetServerObjects(); + } +} diff --git a/src/ServiceHost/Connection/SqlConnection.cs b/src/ServiceHost/Connection/SqlConnection.cs new file mode 100644 index 00000000..5bb92553 --- /dev/null +++ b/src/ServiceHost/Connection/SqlConnection.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// 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.SqlClient; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Factory class to create SqlClientConnections + /// The purpose of the factory is to make it easier to mock out the database + /// in 'offline' unit test scenarios. + /// + public class SqlConnectionFactory : ISqlConnectionFactory + { + /// + /// Creates a new SqlClientConnection object + /// + public ISqlConnection CreateSqlConnection() + { + return new SqlClientConnection(); + } + } + + /// + /// Wrapper class that implements ISqlConnection and hosts a SqlConnection. + /// This wrapper exists primarily for decoupling to support unit testing. + /// + public class SqlClientConnection : ISqlConnection + { + /// + /// the underlying SQL connection + /// + private SqlConnection connection; + + /// + /// Opens a SqlConnection using provided connection string + /// + /// + public void OpenDatabaseConnection(string connectionString) + { + this.connection = new SqlConnection(connectionString); + this.connection.Open(); + } + + /// + /// Gets a list of database server schema objects + /// + /// + public IEnumerable GetServerObjects() + { + // Select the values from sys.tables to give a super basic + // autocomplete experience. This will be replaced by SMO. + SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT name FROM sys.tables"; + command.CommandTimeout = 15; + command.CommandType = CommandType.Text; + var reader = command.ExecuteReader(); + + List results = new List(); + while (reader.Read()) + { + results.Add(reader[0].ToString()); + } + + return results; + } + } +} diff --git a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs new file mode 100644 index 00000000..2cf98484 --- /dev/null +++ b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs @@ -0,0 +1,112 @@ +// +// 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.EditorServices.Connection; +using Microsoft.SqlTools.EditorServices.Protocol.LanguageServer; +using System; +using System.Collections.Generic; + +namespace Microsoft.SqlTools.LanguageSupport +{ + /// + /// Main class for Autocomplete functionality + /// + public class AutoCompleteService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new AutoCompleteService()); + + /// + /// The current autocomplete candidate list + /// + private IEnumerable autoCompleteList; + + /// + /// Gets the current autocomplete candidate list + /// + public IEnumerable AutoCompleteList + { + get + { + return this.autoCompleteList; + } + } + + /// + /// Gets the singleton service instance + /// + public static AutoCompleteService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Update the cached autocomplete candidate list when the user connects to a database + /// + /// + public void UpdateAutoCompleteCache(ISqlConnection connection) + { + this.autoCompleteList = connection.GetServerObjects(); + } + + /// + /// Return the completion item list for the current text position + /// + /// + public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition) + { + var completions = new List(); + + int i = 0; + + // the completion list will be null is user not connected to server + if (this.AutoCompleteList != null) + { + foreach (var autoCompleteItem in this.AutoCompleteList) + { + // convert the completion item candidates into CompletionItems + completions.Add(new CompletionItem() + { + Label = autoCompleteItem, + Kind = CompletionItemKind.Keyword, + Detail = autoCompleteItem + " details", + Documentation = autoCompleteItem + " documentation", + TextEdit = new TextEdit + { + NewText = autoCompleteItem, + Range = new Range + { + Start = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + }, + End = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + 5 + } + } + } + }); + + // only show 50 items + if (++i == 50) + { + break; + } + } + } + return completions.ToArray(); + } + + } +} diff --git a/src/ServiceHost/LanguageSupport/LanguageService.cs b/src/ServiceHost/LanguageSupport/LanguageService.cs index 3ab77697..ff4f4a65 100644 --- a/src/ServiceHost/LanguageSupport/LanguageService.cs +++ b/src/ServiceHost/LanguageSupport/LanguageService.cs @@ -5,6 +5,8 @@ using Microsoft.SqlTools.EditorServices; using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using System.Collections.Generic; namespace Microsoft.SqlTools.LanguageSupport { @@ -13,6 +15,11 @@ namespace Microsoft.SqlTools.LanguageSupport /// public class LanguageService { + /// + /// The cached parse result from previous incremental parse + /// + private ParseResult prevParseResult; + /// /// Gets or sets the current SQL Tools context /// @@ -34,24 +41,38 @@ namespace Microsoft.SqlTools.LanguageSupport /// public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) { - // the commented out snippet is an example of how to create a error marker - // semanticMarkers = new ScriptFileMarker[1]; - // semanticMarkers[0] = new ScriptFileMarker() - // { - // Message = "Error message", - // Level = ScriptFileMarkerLevel.Error, - // ScriptRegion = new ScriptRegion() - // { - // File = scriptFile.FilePath, - // StartLineNumber = 2, - // StartColumnNumber = 2, - // StartOffset = 0, - // EndLineNumber = 4, - // EndColumnNumber = 10, - // EndOffset = 0 - // } - // }; - return new ScriptFileMarker[0]; + // parse current SQL file contents to retrieve a list of errors + ParseOptions parseOptions = new ParseOptions(); + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + prevParseResult, + parseOptions); + + // save previous result for next incremental parse + this.prevParseResult = parseResult; + + // build a list of SQL script file markers from the errors + List markers = new List(); + foreach (var error in parseResult.Errors) + { + markers.Add(new ScriptFileMarker() + { + Message = error.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = new ScriptRegion() + { + File = scriptFile.FilePath, + StartLineNumber = error.Start.LineNumber, + StartColumnNumber = error.Start.ColumnNumber, + StartOffset = 0, + EndLineNumber = error.End.LineNumber, + EndColumnNumber = error.End.ColumnNumber, + EndOffset = 0 + } + }); + } + + return markers.ToArray(); } } } diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs index d6719141..c2952f92 100644 --- a/src/ServiceHost/Server/LanguageServer.cs +++ b/src/ServiceHost/Server/LanguageServer.cs @@ -13,6 +13,8 @@ using System.Text; using System.Threading; using System.Linq; using System; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.LanguageSupport; namespace Microsoft.SqlTools.EditorServices.Protocol.Server { @@ -57,7 +59,22 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); - this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + + this.SetRequestHandler(ConnectionRequest.Type, this.HandleConnectRequest); + + // register an OnConnection callback + ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); + } + + /// + /// Callback for when a user connection is done processing + /// + /// + public Task OnConnection(ISqlConnection sqlConnection) + { + AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection); + return Task.FromResult(true); } /// @@ -122,7 +139,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server /// /// /// - protected Task HandleDidChangeTextDocumentNotification( + protected async Task HandleDidChangeTextDocumentNotification( DidChangeTextDocumentParams textChangeParams, EventContext eventContext) { @@ -133,7 +150,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server // A text change notification can batch multiple change requests foreach (var textChange in textChangeParams.ContentChanges) { - string fileUri = textChangeParams.TextDocument.Uri; + string fileUri = textChangeParams.Uri ?? textChangeParams.TextDocument.Uri; msg.AppendLine(); msg.Append(" File: "); msg.Append(fileUri); @@ -150,23 +167,46 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server Logger.Write(LogLevel.Verbose, msg.ToString()); - this.RunScriptDiagnostics( + await this.RunScriptDiagnostics( changedFiles.ToArray(), editorSession, eventContext); + await Task.FromResult(true); + } + + /// + /// Handle the file open notification + /// + /// + /// + protected Task HandleDidOpenTextDocumentNotification( + DidOpenTextDocumentNotification openParams, + EventContext eventContext) + { + Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); + + // read the SQL file contents into the ScriptFile + ScriptFile openedFile = + editorSession.Workspace.GetFileBuffer( + openParams.Uri, + openParams.Text); + + // run diagnostics on the opened file + this.RunScriptDiagnostics( + new ScriptFile[] { openedFile }, + editorSession, + eventContext); + return Task.FromResult(true); } - protected Task HandleDidOpenTextDocumentNotification( - DidOpenTextDocumentNotification openParams, - EventContext eventContext) - { - Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); - return Task.FromResult(true); - } - - protected Task HandleDidCloseTextDocumentNotification( + /// + /// Handle the close document notication + /// + /// + /// + protected Task HandleDidCloseTextDocumentNotification( TextDocumentIdentifier closeParams, EventContext eventContext) { @@ -240,12 +280,20 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handles the completion list request + /// + /// + /// protected async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - await Task.FromResult(true); + + // get teh current list of completion items and return to client + var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition); + await requestContext.SendResult(completionItems); } protected async Task HandleCompletionResolveRequest( @@ -296,6 +344,24 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handle new connection requests + /// + /// + /// + /// + protected async Task HandleConnectRequest( + ConnectionDetails connectionDetails, + RequestContext requestContext) + { + Logger.Write(LogLevel.Verbose, "HandleConnectRequest"); + + // open connection base on request details + ConnectionResult result = ConnectionService.Instance.Connect(connectionDetails); + + await requestContext.SendResult(result); + } + /// /// Runs script diagnostics on changed files /// diff --git a/src/ServiceHost/Workspace/ScriptFile.cs b/src/ServiceHost/Workspace/ScriptFile.cs index 90d66244..166b50ea 100644 --- a/src/ServiceHost/Workspace/ScriptFile.cs +++ b/src/ServiceHost/Workspace/ScriptFile.cs @@ -106,6 +106,13 @@ namespace Microsoft.SqlTools.EditorServices #region Constructors + /// + /// Add a default constructor for testing + /// + public ScriptFile() + { + } + /// /// Creates a new ScriptFile instance by reading file contents from /// the given TextReader. @@ -433,11 +440,11 @@ namespace Microsoft.SqlTools.EditorServices return new BufferRange(startPosition, endPosition); } - #endregion - - #region Private Methods - - private void SetFileContents(string fileContents) + /// + /// Set the script files contents + /// + /// + public void SetFileContents(string fileContents) { // Split the file contents into lines and trim // any carriage returns from the strings. @@ -451,6 +458,10 @@ namespace Microsoft.SqlTools.EditorServices this.ParseFileContents(); } + #endregion + + #region Private Methods + /// /// Parses the current file contents to get the AST, tokens, /// and parse errors. diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json index 11340892..31dac66d 100644 --- a/src/ServiceHost/project.json +++ b/src/ServiceHost/project.json @@ -5,7 +5,10 @@ "emitEntryPoint": true }, "dependencies": { - "Newtonsoft.Json": "9.0.1" + "Newtonsoft.Json": "9.0.1", + "Microsoft.SqlServer.SqlParser": "140.1.3", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0" }, "frameworks": { "netcoreapp1.0": { diff --git a/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs b/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs new file mode 100644 index 00000000..6796183c --- /dev/null +++ b/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.Test.Connection +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class ConnectionServiceTests + { + #region "Connection tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ConnectToDatabaseTest() + { + // connect to a database instance + var connectionResult = + TestObjects.GetTestConnectionService() + .Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(connectionResult.ConnectionId > 0); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void OnConnectionCallbackHandlerTest() + { + bool callbackInvoked = false; + + // setup connection service with callback + var connectionService = TestObjects.GetTestConnectionService(); + connectionService.RegisterOnConnectionTask( + (sqlConnection) => { + callbackInvoked = true; + return Task.FromResult(true); + } + ); + + // connect to a database instance + var connectionResult = connectionService.Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(callbackInvoked); + } + + #endregion + } +} diff --git a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs new file mode 100644 index 00000000..36aac6b4 --- /dev/null +++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs @@ -0,0 +1,126 @@ +// +// 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.EditorServices; +using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlTools.LanguageSupport; +using Microsoft.SqlTools.Test.Connection; +using Microsoft.SqlTools.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.Test.LanguageServer +{ + /// + /// Tests for the ServiceHost Language Service tests + /// + public class LanguageServiceTests + { + #region "Diagnostics tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseSelectStatementWithoutErrors() + { + // sql statement with no errors + const string sqlWithErrors = "SELECT * FROM sys.objects"; + + // get the test service + LanguageService service = TestObjects.GetTestLanguageService(); + + // parse the sql statement + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there are no errors + Assert.Equal(0, fileMarkers.Length); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseSelectStatementWithError() + { + // sql statement with errors + const string sqlWithErrors = "SELECT *** FROM sys.objects"; + + // get test service + LanguageService service = TestObjects.GetTestLanguageService(); + + // parse sql statement + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there is one error + Assert.Equal(1, fileMarkers.Length); + + // verify the position of the error + Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseMultilineSqlWithErrors() + { + // multiline sql with errors + const string sqlWithErrors = + "SELECT *** FROM sys.objects;\n" + + "GO\n" + + "SELECT *** FROM sys.objects;\n"; + + // get test service + LanguageService service = TestObjects.GetTestLanguageService(); + + // parse sql + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there are two errors + Assert.Equal(2, fileMarkers.Length); + + // check position of first error + Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); + + // check position of second error + Assert.Equal(9, fileMarkers[1].ScriptRegion.StartColumnNumber); + Assert.Equal(3, fileMarkers[1].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[1].ScriptRegion.EndColumnNumber); + Assert.Equal(3, fileMarkers[1].ScriptRegion.EndLineNumber); + } + + #endregion + + #region "Autocomplete Tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void AutocompleteTest() + { + var autocompleteService = TestObjects.GetAutoCompleteService(); + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = connectionService.Connect(TestObjects.GetTestConnectionDetails()); + var sqlConnection = connectionService.ActiveConnections[connectionResult.ConnectionId]; + autocompleteService.UpdateAutoCompleteCache(sqlConnection); + } + + #endregion + } +} + diff --git a/test/ServiceHost.Test/Utility/TestObjects.cs b/test/ServiceHost.Test/Utility/TestObjects.cs new file mode 100644 index 00000000..22fc3f4d --- /dev/null +++ b/test/ServiceHost.Test/Utility/TestObjects.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +//#define USE_LIVE_CONNECTION + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlTools.LanguageSupport; +using Xunit; + +namespace Microsoft.SqlTools.Test.Utility +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class TestObjects + { + /// + /// Creates a test connection service + /// + public static ConnectionService GetTestConnectionService() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new ConnectionService(new TestSqlConnectionFactory()); +#else + // connect to a real server instance + return ConnectionService.Instance; +#endif + } + + /// + /// Creates a test connection details object + /// + public static ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails() + { + UserName = "sa", + Password = "Yukon900", + DatabaseName = "AdventureWorks2016CTP3_2", + ServerName = "sqltools11" + }; + } + + /// + /// Create a test language service instance + /// + /// + public static LanguageService GetTestLanguageService() + { + return new LanguageService(new SqlToolsContext(null, null)); + } + + /// + /// Creates a test autocomplete service instance + /// + public static AutoCompleteService GetAutoCompleteService() + { + return AutoCompleteService.Instance; + } + + /// + /// Creates a test sql connection factory instance + /// + public static ISqlConnectionFactory GetTestSqlConnectionFactory() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new TestSqlConnectionFactory(); +#else + // connect to a real server instance + return ConnectionService.Instance.ConnectionFactory; +#endif + + } + } + + /// + /// Test mock class for SqlConnection wrapper + /// + public class TestSqlConnection : ISqlConnection + { + public void OpenDatabaseConnection(string connectionString) + { + } + + public IEnumerable GetServerObjects() + { + return null; + } + } + + /// + /// Test mock class for SqlConnection factory + /// + public class TestSqlConnectionFactory : ISqlConnectionFactory + { + public ISqlConnection CreateSqlConnection() + { + return new TestSqlConnection(); + } + } +} diff --git a/test/ServiceHost.Test/project.json b/test/ServiceHost.Test/project.json index b7f00724..a248b200 100644 --- a/test/ServiceHost.Test/project.json +++ b/test/ServiceHost.Test/project.json @@ -6,11 +6,13 @@ "dependencies": { "Newtonsoft.Json": "9.0.1", "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0", "xunit": "2.1.0", "dotnet-test-xunit": "1.0.0-rc2-192208-24", - "ServiceHost": { - "target": "project" - } + "ServiceHost": { + "target": "project" + } }, "testRunner": "xunit", "frameworks": {