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
This commit is contained in:
Karl Burtram
2016-07-25 13:04:14 -07:00
committed by GitHub
parent c84dfa34cc
commit 53e26798fc
14 changed files with 894 additions and 41 deletions

15
nuget.config Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration >
<config>
<add key="DefaultPushSource" value="http://SQLISNuget/DS-SSMS/api/v2/package" />
</config>
<packageSources>
<!-- Add the SSMS repo for private requirements -->
<add key="SQLDS - SSMS" value="http://SQLISNuget/DS-SSMS/nuget/" />
</packageSources>
</configuration>

View File

@@ -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
{
/// <summary>
/// Message format for the initial connection request
/// </summary>
public class ConnectionDetails
{
/// <summary>
/// Gets or sets the connection server name
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// Gets or sets the connection database name
/// </summary>
public string DatabaseName { get; set; }
/// <summary>
/// Gets or sets the connection user name
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Gets or sets the connection password
/// </summary>
/// <returns></returns>
public string Password { get; set; }
}
/// <summary>
/// Message format for the connection result response
/// </summary>
public class ConnectionResult
{
/// <summary>
/// Gets or sets the connection id
/// </summary>
public int ConnectionId { get; set; }
/// <summary>
/// Gets or sets any connection error messages
/// </summary>
public string Messages { get; set; }
}
/// <summary>
/// Connect request mapping entry
/// </summary>
public class ConnectionRequest
{
public static readonly
RequestType<ConnectionDetails, ConnectionResult> Type =
RequestType<ConnectionDetails, ConnectionResult>.Create("connection/connect");
}
}

View File

@@ -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
{
/// <summary>
/// Main class for the Connection Management services
/// </summary>
public class ConnectionService
{
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<ConnectionService> instance
= new Lazy<ConnectionService>(() => new ConnectionService());
/// <summary>
/// The SQL connection factory object
/// </summary>
private ISqlConnectionFactory connectionFactory;
/// <summary>
/// The current connection id that was previously used
/// </summary>
private int maxConnectionId = 0;
/// <summary>
/// Active connections lazy dictionary instance
/// </summary>
private Lazy<Dictionary<int, ISqlConnection>> activeConnections
= new Lazy<Dictionary<int, ISqlConnection>>(()
=> new Dictionary<int, ISqlConnection>());
/// <summary>
/// Callback for onconnection handler
/// </summary>
/// <param name="sqlConnection"></param>
public delegate Task OnConnectionHandler(ISqlConnection sqlConnection);
/// <summary>
/// List of onconnection handlers
/// </summary>
private readonly List<OnConnectionHandler> onConnectionActivities = new List<OnConnectionHandler>();
/// <summary>
/// Gets the active connection map
/// </summary>
public Dictionary<int, ISqlConnection> ActiveConnections
{
get
{
return activeConnections.Value;
}
}
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static ConnectionService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Gets the SQL connection factory instance
/// </summary>
public ISqlConnectionFactory ConnectionFactory
{
get
{
if (this.connectionFactory == null)
{
this.connectionFactory = new SqlConnectionFactory();
}
return this.connectionFactory;
}
}
/// <summary>
/// Default constructor is private since it's a singleton class
/// </summary>
private ConnectionService()
{
}
/// <summary>
/// Test constructor that injects dependency interfaces
/// </summary>
/// <param name="testFactory"></param>
public ConnectionService(ISqlConnectionFactory testFactory)
{
this.connectionFactory = testFactory;
}
/// <summary>
/// Open a connection with the specified connection details
/// </summary>
/// <param name="connectionDetails"></param>
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
};
}
/// <summary>
/// Add a new method to be called when the onconnection request is submitted
/// </summary>
/// <param name="activity"></param>
public void RegisterOnConnectionTask(OnConnectionHandler activity)
{
onConnectionActivities.Add(activity);
}
/// <summary>
/// Build a connection string from a connection details instance
/// </summary>
/// <param name="connectionDetails"></param>
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();
}
}
}

View File

@@ -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
{
/// <summary>
/// Interface for the SQL Connection factory
/// </summary>
public interface ISqlConnectionFactory
{
/// <summary>
/// Create a new SQL Connection object
/// </summary>
ISqlConnection CreateSqlConnection();
}
/// <summary>
/// Interface for the SQL Connection wrapper
/// </summary>
public interface ISqlConnection
{
/// <summary>
/// Open a connection to the provided connection string
/// </summary>
/// <param name="connectionString"></param>
void OpenDatabaseConnection(string connectionString);
IEnumerable<string> GetServerObjects();
}
}

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class SqlConnectionFactory : ISqlConnectionFactory
{
/// <summary>
/// Creates a new SqlClientConnection object
/// </summary>
public ISqlConnection CreateSqlConnection()
{
return new SqlClientConnection();
}
}
/// <summary>
/// Wrapper class that implements ISqlConnection and hosts a SqlConnection.
/// This wrapper exists primarily for decoupling to support unit testing.
/// </summary>
public class SqlClientConnection : ISqlConnection
{
/// <summary>
/// the underlying SQL connection
/// </summary>
private SqlConnection connection;
/// <summary>
/// Opens a SqlConnection using provided connection string
/// </summary>
/// <param name="connectionString"></param>
public void OpenDatabaseConnection(string connectionString)
{
this.connection = new SqlConnection(connectionString);
this.connection.Open();
}
/// <summary>
/// Gets a list of database server schema objects
/// </summary>
/// <returns></returns>
public IEnumerable<string> 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<string> results = new List<string>();
while (reader.Read())
{
results.Add(reader[0].ToString());
}
return results;
}
}
}

View File

@@ -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
{
/// <summary>
/// Main class for Autocomplete functionality
/// </summary>
public class AutoCompleteService
{
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<AutoCompleteService> instance
= new Lazy<AutoCompleteService>(() => new AutoCompleteService());
/// <summary>
/// The current autocomplete candidate list
/// </summary>
private IEnumerable<string> autoCompleteList;
/// <summary>
/// Gets the current autocomplete candidate list
/// </summary>
public IEnumerable<string> AutoCompleteList
{
get
{
return this.autoCompleteList;
}
}
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static AutoCompleteService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// </summary>
/// <param name="connection"></param>
public void UpdateAutoCompleteCache(ISqlConnection connection)
{
this.autoCompleteList = connection.GetServerObjects();
}
/// <summary>
/// Return the completion item list for the current text position
/// </summary>
/// <param name="textDocumentPosition"></param>
public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition)
{
var completions = new List<CompletionItem>();
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();
}
}
}

View File

@@ -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
/// </summary>
public class LanguageService
{
/// <summary>
/// The cached parse result from previous incremental parse
/// </summary>
private ParseResult prevParseResult;
/// <summary>
/// Gets or sets the current SQL Tools context
/// </summary>
@@ -34,24 +41,38 @@ namespace Microsoft.SqlTools.LanguageSupport
/// <param name="scriptFile"></param>
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<ScriptFileMarker> markers = new List<ScriptFileMarker>();
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();
}
}
}

View File

@@ -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);
}
/// <summary>
/// Callback for when a user connection is done processing
/// </summary>
/// <param name="sqlConnection"></param>
public Task OnConnection(ISqlConnection sqlConnection)
{
AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection);
return Task.FromResult(true);
}
/// <summary>
@@ -122,7 +139,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Handle the file open notification
/// </summary>
/// <param name="openParams"></param>
/// <param name="eventContext"></param>
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(
/// <summary>
/// Handle the close document notication
/// </summary>
/// <param name="closeParams"></param>
/// <param name="eventContext"></param>
protected Task HandleDidCloseTextDocumentNotification(
TextDocumentIdentifier closeParams,
EventContext eventContext)
{
@@ -240,12 +280,20 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server
await Task.FromResult(true);
}
/// <summary>
/// Handles the completion list request
/// </summary>
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
protected async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> 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);
}
/// <summary>
/// Handle new connection requests
/// </summary>
/// <param name="connectionDetails"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
protected async Task HandleConnectRequest(
ConnectionDetails connectionDetails,
RequestContext<ConnectionResult> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleConnectRequest");
// open connection base on request details
ConnectionResult result = ConnectionService.Instance.Connect(connectionDetails);
await requestContext.SendResult(result);
}
/// <summary>
/// Runs script diagnostics on changed files
/// </summary>

View File

@@ -106,6 +106,13 @@ namespace Microsoft.SqlTools.EditorServices
#region Constructors
/// <summary>
/// Add a default constructor for testing
/// </summary>
public ScriptFile()
{
}
/// <summary>
/// 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)
/// <summary>
/// Set the script files contents
/// </summary>
/// <param name="fileContents"></param>
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
/// <summary>
/// Parses the current file contents to get the AST, tokens,
/// and parse errors.

View File

@@ -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": {

View File

@@ -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
{
/// <summary>
/// Tests for the ServiceHost Connection Service tests
/// </summary>
public class ConnectionServiceTests
{
#region "Connection tests"
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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);
}
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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
}
}

View File

@@ -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
{
/// <summary>
/// Tests for the ServiceHost Language Service tests
/// </summary>
public class LanguageServiceTests
{
#region "Diagnostics tests"
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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);
}
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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);
}
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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"
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[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
}
}

View File

@@ -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
{
/// <summary>
/// Tests for the ServiceHost Connection Service tests
/// </summary>
public class TestObjects
{
/// <summary>
/// Creates a test connection service
/// </summary>
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
}
/// <summary>
/// Creates a test connection details object
/// </summary>
public static ConnectionDetails GetTestConnectionDetails()
{
return new ConnectionDetails()
{
UserName = "sa",
Password = "Yukon900",
DatabaseName = "AdventureWorks2016CTP3_2",
ServerName = "sqltools11"
};
}
/// <summary>
/// Create a test language service instance
/// </summary>
/// <returns></returns>
public static LanguageService GetTestLanguageService()
{
return new LanguageService(new SqlToolsContext(null, null));
}
/// <summary>
/// Creates a test autocomplete service instance
/// </summary>
public static AutoCompleteService GetAutoCompleteService()
{
return AutoCompleteService.Instance;
}
/// <summary>
/// Creates a test sql connection factory instance
/// </summary>
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
}
}
/// <summary>
/// Test mock class for SqlConnection wrapper
/// </summary>
public class TestSqlConnection : ISqlConnection
{
public void OpenDatabaseConnection(string connectionString)
{
}
public IEnumerable<string> GetServerObjects()
{
return null;
}
}
/// <summary>
/// Test mock class for SqlConnection factory
/// </summary>
public class TestSqlConnectionFactory : ISqlConnectionFactory
{
public ISqlConnection CreateSqlConnection()
{
return new TestSqlConnection();
}
}
}

View File

@@ -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": {