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..a2b506aa --- /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.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.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..afbf4ab4 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionService.cs @@ -0,0 +1,207 @@ +// +// 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; +using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; + +namespace Microsoft.SqlTools.ServiceLayer.Connection +{ + /// + /// Main class for the Connection Management services + /// + public class ConnectionService + { + #region Singleton Instance Implementation + + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new ConnectionService()); + + /// + /// Gets the singleton service instance + /// + public static ConnectionService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Default constructor is private since it's a singleton class + /// + private ConnectionService() + { + } + + #endregion + + #region Properties + + /// + /// 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 SQL connection factory instance + /// + public ISqlConnectionFactory ConnectionFactory + { + get + { + if (this.connectionFactory == null) + { + this.connectionFactory = new SqlConnectionFactory(); + } + return this.connectionFactory; + } + } + + #endregion + + /// + /// Test constructor that injects dependency interfaces + /// + /// + public ConnectionService(ISqlConnectionFactory testFactory) + { + this.connectionFactory = testFactory; + } + + #region Public Methods + + /// + /// 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 + }; + } + + public void Initialize(ServiceHost serviceHost) + { + // Register request and event handlers with the Service Host + serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest); + } + + /// + /// Add a new method to be called when the onconnection request is submitted + /// + /// + public void RegisterOnConnectionTask(OnConnectionHandler activity) + { + onConnectionActivities.Add(activity); + } + + #endregion + + #region Request Handlers + + /// + /// 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); + } + + #endregion + + #region Private Helpers + + /// + /// 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(); + } + + #endregion + } +} diff --git a/src/ServiceHost/Connection/ISqlConnection.cs b/src/ServiceHost/Connection/ISqlConnection.cs new file mode 100644 index 00000000..3e5fbdfe --- /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.ServiceLayer.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..6ad90b39 --- /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.ServiceLayer.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/LanguageServices/AutoCompleteService.cs b/src/ServiceHost/LanguageServices/AutoCompleteService.cs new file mode 100644 index 00000000..67981ae5 --- /dev/null +++ b/src/ServiceHost/LanguageServices/AutoCompleteService.cs @@ -0,0 +1,123 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Main class for Autocomplete functionality + /// + public class AutoCompleteService + { + #region Singleton Instance Implementation + + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new AutoCompleteService()); + + /// + /// Gets the singleton service instance + /// + public static AutoCompleteService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Default, parameterless constructor. + /// TODO: Figure out how to make this truely singleton even with dependency injection for tests + /// + public AutoCompleteService() + { + } + + #endregion + + /// + /// Gets the current autocomplete candidate list + /// + public IEnumerable AutoCompleteList { get; private set; } + + public void InitializeService(ServiceHost serviceHost) + { + // Register a callback for when a connection is created + ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); + } + + /// + /// Update the cached autocomplete candidate list when the user connects to a database + /// + /// + public async Task UpdateAutoCompleteCache(ISqlConnection connection) + { + AutoCompleteList = connection.GetServerObjects(); + await Task.FromResult(0); + } + + /// + /// 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/LanguageServices/LanguageService.cs b/src/ServiceHost/LanguageServices/LanguageService.cs index d88cfd92..d40aa5fc 100644 --- a/src/ServiceHost/LanguageServices/LanguageService.cs +++ b/src/ServiceHost/LanguageServices/LanguageService.cs @@ -4,6 +4,7 @@ // using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; @@ -14,6 +15,8 @@ using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.WorkspaceServices; using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts; using System.Linq; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Location = Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts.Location; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -35,8 +38,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Default, parameterless constructor. + /// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// - private LanguageService() + public LanguageService() { } @@ -62,8 +66,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// private SqlToolsContext Context { get; set; } + /// + /// The cached parse result from previous incremental parse + /// + private ParseResult prevParseResult; + #endregion + #region Public Methods + public void InitializeService(ServiceHost serviceHost, SqlToolsContext context) { // Register the requests that this service will handle @@ -91,6 +102,48 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Context = context; } + /// + /// Gets a list of semantic diagnostic marks for the provided script file + /// + /// + public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) + { + // 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(); + } + + #endregion + #region Request Handlers private static async Task HandleDefinitionRequest( @@ -204,32 +257,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices #region Private Helpers - /// - /// Gets a list of semantic diagnostic marks for the provided script file - /// - /// - private 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]; - } - /// /// Runs script diagnostics on changed files /// diff --git a/src/ServiceHost/Program.cs b/src/ServiceHost/Program.cs index 53c15d3a..02369811 100644 --- a/src/ServiceHost/Program.cs +++ b/src/ServiceHost/Program.cs @@ -39,6 +39,7 @@ namespace Microsoft.SqlTools.ServiceLayer // Initialize the services that will be hosted here WorkspaceService.Instance.InitializeService(serviceHost); + AutoCompleteService.Instance.InitializeService(serviceHost); LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); // Start the service diff --git a/src/ServiceHost/WorkspaceServices/Contracts/ScriptFile.cs b/src/ServiceHost/WorkspaceServices/Contracts/ScriptFile.cs index b28a88db..708bae70 100644 --- a/src/ServiceHost/WorkspaceServices/Contracts/ScriptFile.cs +++ b/src/ServiceHost/WorkspaceServices/Contracts/ScriptFile.cs @@ -100,6 +100,13 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts #region Constructors + /// + /// Add a default constructor for testing + /// + public ScriptFile() + { + } + /// /// Creates a new ScriptFile instance by reading file contents from /// the given TextReader. @@ -421,11 +428,11 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts 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. @@ -439,6 +446,10 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts this.ParseFileContents(); } + #endregion + + #region Private Methods + /// /// Parses the current file contents to get the AST, tokens, /// and parse errors. diff --git a/src/ServiceHost/WorkspaceServices/WorkspaceService.cs b/src/ServiceHost/WorkspaceServices/WorkspaceService.cs index a0b537a0..13575121 100644 --- a/src/ServiceHost/WorkspaceServices/WorkspaceService.cs +++ b/src/ServiceHost/WorkspaceServices/WorkspaceService.cs @@ -35,7 +35,11 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices get { return instance.Value; } } - private WorkspaceService() + /// + /// Default, parameterless constructor. + /// TODO: Figure out how to make this truely singleton even with dependency injection for tests + /// + public WorkspaceService() { ConfigChangeCallbacks = new List(); TextDocChangeCallbacks = new List(); 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..ed39ce2b --- /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.ServiceLayer.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..8f85869d --- /dev/null +++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts; +using Microsoft.SqlTools.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.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..c506d600 --- /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.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +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(); + } + + /// + /// 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": {