diff --git a/.gitignore b/.gitignore index c87915eb..ba6a7266 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ project.lock.json *.user *.userosscache *.sln.docstates +*.exe # Build results [Dd]ebug/ @@ -30,9 +31,10 @@ msbuild.err msbuild.wrn # code coverage artifacts +coverage.xml node_modules packages -coverage.xml +reports # Cross building rootfs cross/rootfs/ diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs index be778f92..949206de 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs @@ -6,10 +6,17 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.SqlServer.Management.SmoMetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices @@ -40,19 +47,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Default, parameterless constructor. - /// TODO: Figure out how to make this truely singleton even with dependency injection for tests + /// Internal constructor for use in test cases only /// - public AutoCompleteService() + internal AutoCompleteService() { } #endregion - // Dictionary of unique intellisense caches for each Connection - private Dictionary caches = - new Dictionary(new ConnectionSummaryComparer()); - private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary - private ConnectionService connectionService = null; /// @@ -77,6 +79,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices public void InitializeService(ServiceHost serviceHost) { + // Register auto-complete request handler + serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); + // Register a callback for when a connection is created ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache); @@ -84,12 +89,29 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); } - /// - /// Intellisense cache count access for testing. - /// - internal int GetCacheCount() + /// + /// Auto-complete completion provider request callback + /// + /// + /// + /// + private static async Task HandleCompletionRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) { - return caches.Count; + // get the current list of completion items and return to client + var scriptFile = WorkspaceService.Instance.Workspace.GetFile( + textDocumentPosition.TextDocument.Uri); + + ConnectionInfo connInfo; + ConnectionService.Instance.TryFindConnection( + scriptFile.ClientFilePath, + out connInfo); + + var completionItems = Instance.GetCompletionItems( + textDocumentPosition, scriptFile, connInfo); + + await requestContext.SendResult(completionItems); } /// @@ -99,47 +121,163 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) { - await Task.Run( () => - { - lock(cachesLock) - { - IntellisenseCache cache; - if( caches.TryGetValue(summary, out cache) ) - { - cache.ReferenceCount--; + await Task.FromResult(0); + // await Task.Run( () => + // { + // lock(cachesLock) + // { + // AutoCompleteCache cache; + // if( caches.TryGetValue(summary, out cache) ) + // { + // cache.ReferenceCount--; - // Remove unused caches - if( cache.ReferenceCount == 0 ) - { - caches.Remove(summary); - } - } - } - }); + // // Remove unused caches + // if( cache.ReferenceCount == 0 ) + // { + // caches.Remove(summary); + // } + // } + // } + // }); } - /// /// Update the cached autocomplete candidate list when the user connects to a database /// /// public async Task UpdateAutoCompleteCache(ConnectionInfo info) { - if (info != null) + await Task.Run( () => { - IntellisenseCache cache; - lock(cachesLock) + if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(info.OwnerUri)) { - if(!caches.TryGetValue(info.ConnectionDetails, out cache)) - { - cache = new IntellisenseCache(info.Factory, info.ConnectionDetails); - caches[cache.DatabaseInfo] = cache; - } - cache.ReferenceCount++; + var srvConn = ConnectionService.GetServerConnection(info); + var displayInfoProvider = new MetadataDisplayInfoProvider(); + var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn); + var binder = BinderProvider.CreateBinder(metadataProvider); + + LanguageService.Instance.ScriptParseInfoMap.Add(info.OwnerUri, + new ScriptParseInfo() + { + Binder = binder, + MetadataProvider = metadataProvider, + MetadataDisplayInfoProvider = displayInfoProvider + }); + + var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); + + LanguageService.Instance.ParseAndBind(scriptFile, info); } - - await cache.UpdateCache(); + }); + } + + /// + /// Find the position of the previous delimeter for autocomplete token replacement. + /// SQL Parser may have similar functionality in which case we'll delete this method. + /// + /// + /// + /// + /// + private int PositionOfPrevDelimeter(string sql, int startRow, int startColumn) + { + if (string.IsNullOrWhiteSpace(sql)) + { + return 1; } + + int prevLineColumns = 0; + for (int i = 0; i < startRow; ++i) + { + while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length) + { + ++prevLineColumns; + } + ++prevLineColumns; + } + + startColumn += prevLineColumns; + + if (startColumn - 1 < sql.Length) + { + while (--startColumn >= prevLineColumns) + { + if (sql[startColumn] == ' ' + || sql[startColumn] == '\t' + || sql[startColumn] == '\n' + || sql[startColumn] == '.' + || sql[startColumn] == '+' + || sql[startColumn] == '-' + || sql[startColumn] == '*' + || sql[startColumn] == '>' + || sql[startColumn] == '<' + || sql[startColumn] == '=' + || sql[startColumn] == '/' + || sql[startColumn] == '%') + { + break; + } + } + } + + return startColumn + 1 - prevLineColumns; + } + + /// + /// Determines whether a reparse and bind is required to provide autocomplete + /// + /// + /// TEMP: Currently hard-coded to false for perf + private bool RequiresReparse(ScriptParseInfo info) + { + return false; + } + + /// + /// Converts a list of Declaration objects to CompletionItem objects + /// since VS Code expects CompletionItems but SQL Parser works with Declarations + /// + /// + /// + /// + /// + private CompletionItem[] ConvertDeclarationsToCompletionItems( + IEnumerable suggestions, + int row, + int startColumn, + int endColumn) + { + List completions = new List(); + foreach (var autoCompleteItem in suggestions) + { + // convert the completion item candidates into CompletionItems + completions.Add(new CompletionItem() + { + Label = autoCompleteItem.Title, + Kind = CompletionItemKind.Keyword, + Detail = autoCompleteItem.Title, + Documentation = autoCompleteItem.Description, + TextEdit = new TextEdit + { + NewText = autoCompleteItem.Title, + Range = new Range + { + Start = new Position + { + Line = row, + Character = startColumn + }, + End = new Position + { + Line = row, + Character = endColumn + } + } + } + }); + } + + return completions.ToArray(); } /// @@ -147,22 +285,48 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// This method does not await cache builds since it expects to return quickly /// /// - public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition) + public CompletionItem[] GetCompletionItems( + TextDocumentPosition textDocumentPosition, + ScriptFile scriptFile, + ConnectionInfo connInfo) { - // Try to find a cache for the document's backing connection (if available) - // If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners - // behave well, there should be a cache for any actively connected document. This also helps skip documents - // that are not backed by a SQL connection - ConnectionInfo info; - IntellisenseCache cache; - if (ConnectionServiceInstance.TryFindConnection(textDocumentPosition.TextDocument.Uri, out info) - && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache)) + string filePath = textDocumentPosition.TextDocument.Uri; + + // Take a reference to the list at a point in time in case we update and replace the list + if (connInfo == null + || !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri)) { - return cache.GetAutoCompleteItems(textDocumentPosition).ToArray(); + return new CompletionItem[0]; } - - return new CompletionItem[0]; + + // reparse and bind the SQL statement if needed + var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri]; + if (RequiresReparse(scriptParseInfo)) + { + LanguageService.Instance.ParseAndBind(scriptFile, connInfo); + } + + if (scriptParseInfo.ParseResult == null) + { + return new CompletionItem[0]; + } + + // get the completion list from SQL Parser + var suggestions = Resolver.FindCompletions( + scriptParseInfo.ParseResult, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.Position.Character + 1, + scriptParseInfo.MetadataDisplayInfoProvider); + + // convert the suggestion list to the VS Code format + return ConvertDeclarationsToCompletionItems( + suggestions, + textDocumentPosition.Position.Line, + PositionOfPrevDelimeter( + scriptFile.Contents, + textDocumentPosition.Position.Line, + textDocumentPosition.Position.Character), + textDocumentPosition.Position.Character); } - } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs deleted file mode 100644 index 3fa1ed29..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs +++ /dev/null @@ -1,135 +0,0 @@ -// -// 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.Common; -using System.Threading.Tasks; -using Microsoft.SqlServer.Management.SqlParser.Intellisense; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; -using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; -using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; - -namespace Microsoft.SqlTools.ServiceLayer.LanguageServices -{ - internal class IntellisenseCache - { - /// - /// connection used to query for intellisense info - /// - private DbConnection connection; - - /// - /// Number of documents (URI's) that are using the cache for the same database. - /// The autocomplete service uses this to remove unreferenced caches. - /// - public int ReferenceCount { get; set; } - - public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails) - { - ReferenceCount = 0; - DatabaseInfo = connectionDetails.Clone(); - - // TODO error handling on this. Intellisense should catch or else the service should handle - connection = connectionFactory.CreateSqlConnection(ConnectionService.BuildConnectionString(connectionDetails)); - connection.Open(); - } - - /// - /// Used to identify a database for which this cache is used - /// - public ConnectionSummary DatabaseInfo - { - get; - private set; - } - /// - /// Gets the current autocomplete candidate list - /// - public IEnumerable AutoCompleteList { get; private set; } - - public async Task UpdateCache() - { - DbCommand command = connection.CreateCommand(); - command.CommandText = "SELECT name FROM sys.tables"; - command.CommandTimeout = 15; - command.CommandType = CommandType.Text; - var reader = await command.ExecuteReaderAsync(); - - List results = new List(); - while (await reader.ReadAsync()) - { - results.Add(reader[0].ToString()); - } - - AutoCompleteList = results; - await Task.FromResult(0); - } - - public List GetAutoCompleteItems(TextDocumentPosition textDocumentPosition) - { - List completions = new List(); - - // Take a reference to the list at a point in time in case we update and replace the list - //var suggestions = AutoCompleteList; - if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri)) - { - return completions; - } - - var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri]; - var suggestions = Resolver.FindCompletions( - scriptParseInfo.ParseResult, - textDocumentPosition.Position.Line + 1, - textDocumentPosition.Position.Character + 1, - scriptParseInfo.MetadataDisplayInfoProvider); - - int i = 0; - - // the completion list will be null is user not connected to server - if (this.AutoCompleteList != null) - { - - foreach (var autoCompleteItem in suggestions) - { - // convert the completion item candidates into CompletionItems - completions.Add(new CompletionItem() - { - Label = autoCompleteItem.Title, - Kind = CompletionItemKind.Keyword, - Detail = autoCompleteItem.Title + " details", - Documentation = autoCompleteItem.Title + " documentation", - TextEdit = new TextEdit - { - NewText = autoCompleteItem.Title, - 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; - } - } -} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 611e8c81..d414b41e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; @@ -20,9 +19,8 @@ using Microsoft.SqlServer.Management.SqlParser.Parser; using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlServer.Management.SqlParser.Binder; -using Microsoft.SqlServer.Management.SmoMetadataProvider; using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -40,17 +38,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private Lazy> scriptParseInfoMap = new Lazy>(() => new Dictionary()); - internal class ScriptParseInfo - { - public IBinder Binder { get; set; } - - public ParseResult ParseResult { get; set; } - - public SmoMetadataProvider MetadataProvider { get; set; } - - public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } - } - internal Dictionary ScriptParseInfoMap { get @@ -93,21 +80,20 @@ 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 + /// + /// Initializes the Language Service instance + /// + /// + /// public void InitializeService(ServiceHost serviceHost, SqlToolsContext context) { // Register the requests that this service will handle serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest); - serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest); serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest); serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest); @@ -135,52 +121,69 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Context = context; } + /// + /// Parses the SQL text and binds it to the SMO metadata provider if connected + /// + /// + /// + /// + public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) + { + ScriptParseInfo parseInfo = null; + if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath)) + { + parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath]; + } + + // parse current SQL file contents to retrieve a list of errors + ParseOptions parseOptions = new ParseOptions(); + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo != null ? parseInfo.ParseResult : null, + parseOptions); + + // save previous result for next incremental parse + if (parseInfo != null) + { + parseInfo.ParseResult = parseResult; + } + + if (connInfo != null) + { + try + { + List parseResults = new List(); + parseResults.Add(parseResult); + parseInfo.Binder.Bind( + parseResults, + connInfo.ConnectionDetails.DatabaseName, + BindMode.Batch); + } + catch (ConnectionException) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + catch (SqlParserInternalBinderError) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + } + + return parseResult; + } + /// /// 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; - ConnectionInfo connInfo; - bool isConnected = ConnectionService.Instance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); - if (isConnected) - { - if (!this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath)) - { - var srvConn = ConnectionService.GetServerConnection(connInfo); - var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn); - var binder = BinderProvider.CreateBinder(metadataProvider); - var displayInfoProvider = new MetadataDisplayInfoProvider(); - - this.ScriptParseInfoMap.Add(scriptFile.ClientFilePath, - new ScriptParseInfo() - { - Binder = binder, - ParseResult = parseResult, - MetadataProvider = metadataProvider, - MetadataDisplayInfoProvider = displayInfoProvider - }); - } - - ScriptParseInfo parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath]; - parseInfo.ParseResult = parseResult; - List parseResults = new List(); - parseResults.Add(parseResult); - parseInfo.Binder.Bind( - parseResults, - connInfo.ConnectionDetails.DatabaseName, - BindMode.Batch); - } + ConnectionService.Instance.TryFindConnection( + scriptFile.ClientFilePath, + out connInfo); + + var parseResult = ParseAndBind(scriptFile, connInfo); // build a list of SQL script file markers from the errors List markers = new List(); @@ -226,17 +229,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices await Task.FromResult(true); } - private static async Task HandleCompletionRequest( - TextDocumentPosition textDocumentPosition, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - - // get the current list of completion items and return to client - var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition); - await requestContext.SendResult(completionItems); - } - private static async Task HandleCompletionResolveRequest( CompletionItem completionItem, RequestContext requestContext) @@ -305,7 +297,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices await Task.FromResult(true); } - /// /// Handles text document change events @@ -506,7 +497,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Message = scriptFileMarker.Message, Range = new Range { - // TODO: What offsets should I use? Start = new Position { Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs new file mode 100644 index 00000000..4da2c57e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlServer.Management.SmoMetadataProvider; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Class for storing cached metadata regarding a parsed SQL file + /// + internal class ScriptParseInfo + { + /// + /// Gets or sets the SMO binder for schema-aware intellisense + /// + public IBinder Binder { get; set; } + + /// + /// Gets or sets the previous SQL parse result + /// + public ParseResult ParseResult { get; set; } + + /// + /// Gets or set the SMO metadata provider that's bound to the current connection + /// + /// + public SmoMetadataProvider MetadataProvider { get; set; } + + /// + /// Gets or sets the SMO metadata display info provider + /// + /// + public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs index f44e2b65..74592ae5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs @@ -34,9 +34,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts public string FilePath { get; private set; } /// - /// Gets the path which the editor client uses to identify this file. + /// Gets or sets the path which the editor client uses to identify this file. + /// Setter for testing purposes only /// - public string ClientFilePath { get; private set; } + public string ClientFilePath { get; internal set; } /// /// Gets or sets a boolean that determines whether @@ -52,7 +53,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts public bool IsInMemory { get; private set; } /// - /// Gets a string containing the full contents of the file. + /// Gets or sets a string containing the full contents of the file. + /// Setter for testing purposes only /// public string Contents { @@ -60,6 +62,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts { return string.Join("\r\n", this.FileLines); } + set + { + this.FileLines = value != null ? value.Split('\n') : null; + } } /// diff --git a/test/CodeCoverage/codecoverage.bat b/test/CodeCoverage/codecoverage.bat index 767d68ef..098ec1a1 100644 --- a/test/CodeCoverage/codecoverage.bat +++ b/test/CodeCoverage/codecoverage.bat @@ -1,11 +1,25 @@ SET WORKINGDIR=%~dp0 -rmdir %WORKINGDIR%reports\ /S /Q -del %WORKINGDIR%coverage.xml -mkdir reports + +REM clean-up results from previous run +RMDIR %WORKINGDIR%reports\ /S /Q +DEL %WORKINGDIR%coverage.xml +MKDIR reports + +REM backup current project.json COPY /Y %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK + +REM switch PDB type to Full since that is required by OpenCover for now +REM we should remove this step on OpenCover supports portable PDB cscript /nologo ReplaceText.vbs %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json portable full + +REM rebuild the SqlToolsService project dotnet build %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json + +REM run the tests through OpenCover and generate a report "%WORKINGDIR%packages\OpenCover.4.6.519\tools\OpenCover.Console.exe" -register:user -target:dotnet.exe -targetargs:"test %WORKINGDIR%..\Microsoft.SqlTools.ServiceLayer.Test\project.json" -oldstyle -filter:"+[Microsoft.SqlTools.*]* -[xunit*]*" -output:coverage.xml -searchdirs:%WORKINGDIR%..\Microsoft.SqlTools.ServiceLayer.Test\bin\Debug\netcoreapp1.0 "%WORKINGDIR%packages\ReportGenerator.2.4.5.0\tools\ReportGenerator.exe" "-reports:coverage.xml" "-targetdir:%WORKINGDIR%\reports" + +REM restore original project.json COPY /Y %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json +DEL %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK EXIT diff --git a/test/CodeCoverage/gulpfile.js b/test/CodeCoverage/gulpfile.js index 2692900d..38b1576c 100644 --- a/test/CodeCoverage/gulpfile.js +++ b/test/CodeCoverage/gulpfile.js @@ -1,5 +1,4 @@ var gulp = require('gulp'); -//var install = require('gulp-install');; var del = require('del'); var request = require('request'); var fs = require('fs'); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index d89ac3dc..0725c209 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -22,6 +22,7 @@ using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.Test.QueryExecution; using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.Test.Utility; @@ -38,91 +39,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices { #region "Diagnostics tests" - - // [Fact] - // public void TestParseWideWorldImporters() - // { - // var sql = File.ReadAllText(@"e:\data\script.sql"); - // //string sql = @"SELECT "; - // ParseOptions parseOptions = new ParseOptions(); - // ParseResult parseResult = Parser.IncrementalParse( - // sql, - // null, - // parseOptions); - // } - - // [Fact] - // public void TestSmo() - // { - // SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); - // connectionBuilder["Data Source"] = "sqltools11"; - // connectionBuilder["Integrated Security"] = false; - // connectionBuilder["User Id"] = "sa"; - // connectionBuilder["Password"] = "Yukon900"; - // connectionBuilder["Initial Catalog"] = "master"; - // string connectionString = connectionBuilder.ToString(); - - // var conn = new SqlConnection(connectionString); - // var sqlConn = new ServerConnection(conn); - - // var server = new Server(sqlConn); - // string s = ""; - // foreach (Database db2 in server.Databases) - // { - // s += db2.Name; - // } - - // var metadata = SmoMetadataProvider.CreateConnectedProvider(sqlConn); - // var db = metadata.Server.Databases["master"]; - // } - - // [Fact] - // public void TestSmoMetadataProvider() - // { - // SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); - // //connectionBuilder["Data Source"] = "sqltools11"; - // connectionBuilder["Data Source"] = "localhost"; - // connectionBuilder["Integrated Security"] = false; - // connectionBuilder["User Id"] = "sa"; - // connectionBuilder["Password"] = "Yukon900"; - // connectionBuilder["Initial Catalog"] = "master"; - - // try - // { - // var sqlConnection = new SqlConnection(connectionBuilder.ToString()); - // var connection = new ServerConnection(sqlConnection); - // var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(connection); - // var binder = BinderProvider.CreateBinder(metadataProvider); - // var displayInfoProvider = new MetadataDisplayInfoProvider(); - - // //string sql = @"SELECT * FROM sys.objects;"; - - // string sql = @"SELECT "; - - // ParseOptions parseOptions = new ParseOptions(); - // ParseResult parseResult = Parser.IncrementalParse( - // sql, - // null, - // parseOptions); - - // List parseResults = new List(); - // parseResults.Add(parseResult); - // binder.Bind(parseResults, "master", BindMode.Batch); - - // var comp = Resolver.FindCompletions(parseResult, 1, 8, displayInfoProvider); - // comp.Add(null); - // } - // finally - // { - // // Check if we failed to create a binder object. If so, we temporarely - // // use a no-op binder which has the effect of turning off binding. We - // // also set a timer that after the specified timeout expires removes - // // the no-op timer (object becomes dead) which would give clients of - // // this class an opportunity to remove it and create a new one. - // } - // } - - /// /// Verify that the latest SqlParser (2016 as of this writing) is used by default /// @@ -234,6 +150,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices #region "Autocomplete Tests" + // This test currently requires a live database connection to initialize + // SMO connected metadata provider. Since we don't want a live DB dependency + // in the CI unit tests this scenario is currently disabled. + //[Fact] + public void AutoCompleteFindCompletions() + { + TextDocumentPosition textDocument; + ConnectionInfo connInfo; + ScriptFile scriptFile; + Common.GetAutoCompleteTestObjects(out textDocument, out scriptFile, out connInfo); + + textDocument.Position.Character = 7; + scriptFile.Contents = "select "; + + var autoCompleteService = AutoCompleteService.Instance; + var completions = autoCompleteService.GetCompletionItems( + textDocument, + scriptFile, + connInfo); + + Assert.True(completions.Length > 0); + } + /// /// Creates a mock db command that returns a predefined result set /// @@ -261,166 +200,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices return connectionMock.Object; } -#if false - /// - /// Verify that the autocomplete service returns tables for the current connection as suggestions - /// - [Fact] - public void TablesAreReturnedAsAutocompleteSuggestions() - { - // Result set for the query of database tables - Dictionary[] data = - { - new Dictionary { {"name", "master" } }, - new Dictionary { {"name", "model" } } - }; - - var mockFactory = new Mock(); - mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) - .Returns(CreateMockDbConnection(new[] {data})); - - var connectionService = new ConnectionService(mockFactory.Object); - var autocompleteService = new AutoCompleteService(); - autocompleteService.ConnectionServiceInstance = connectionService; - autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); - - // 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.TextDocument = new TextDocumentIdentifier(); - position.TextDocument.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); - } -#endif - - /// - /// 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 = new ConnectionService(TestObjects.GetTestSqlConnectionFactory()); - 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()); - } - -#if false - /// - /// Verify that two different intellisense caches and corresponding autocomplete - /// suggestions are provided for two documents with different connections. - /// - [Fact] - public void TwoCachesAreCreatedForTwoDocumentsWithDifferentConnections() - { - const string testDb1 = "my_db"; - const string testDb2 = "my_other_db"; - - // 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.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb1)))) - .Returns(CreateMockDbConnection(new[] {data1})); - mockFactory.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb2)))) - .Returns(CreateMockDbConnection(new[] {data2})); - - var connectionService = new ConnectionService(mockFactory.Object); - var autocompleteService = new AutoCompleteService(); - autocompleteService.ConnectionServiceInstance = connectionService; - autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); - - // Open connections - // The cache should get updated as part of this - ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); - connectionRequest.OwnerUri = "file:///my/first/sql/file.sql"; - connectionRequest.Connection.DatabaseName = testDb1; - 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 = testDb2; - 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.TextDocument = new TextDocumentIdentifier(); - position.TextDocument.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.TextDocument = new TextDocumentIdentifier(); - position2.TextDocument.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); - } -#endif #endregion } } - - - - diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 6d265de2..878073a0 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -9,14 +9,19 @@ using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; +using Microsoft.SqlServer.Management.SmoMetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Test.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Moq; using Moq.Protected; @@ -36,6 +41,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public const int StandardColumns = 5; + public static string TestServer { get; set; } + + public static string TestDatabase { get; set; } + + static Common() + { + TestServer = "sqltools11"; + TestDatabase = "master"; + } + public static Dictionary[] StandardTestData { get { return GetTestData(StandardRows, StandardColumns); } @@ -122,8 +137,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { UserName = "sa", Password = "Yukon900", - DatabaseName = "AdventureWorks2016CTP3_2", - ServerName = "sqltools11" + DatabaseName = Common.TestDatabase, + ServerName = Common.TestServer }; return new ConnectionInfo(CreateMockFactory(data, throwOnRead), OwnerUri, connDetails); @@ -133,6 +148,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution #region Service Mocking + public static void GetAutoCompleteTestObjects( + out TextDocumentPosition textDocument, + out ScriptFile scriptFile, + out ConnectionInfo connInfo + ) + { + textDocument = new TextDocumentPosition(); + textDocument.TextDocument = new TextDocumentIdentifier(); + textDocument.TextDocument.Uri = Common.OwnerUri; + textDocument.Position = new Position(); + textDocument.Position.Line = 0; + textDocument.Position.Character = 0; + + connInfo = Common.CreateTestConnectionInfo(null, false); + var srvConn = ConnectionService.GetServerConnection(connInfo); + var displayInfoProvider = new MetadataDisplayInfoProvider(); + var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn); + var binder = BinderProvider.CreateBinder(metadataProvider); + + LanguageService.Instance.ScriptParseInfoMap.Add(textDocument.TextDocument.Uri, + new ScriptParseInfo() + { + Binder = binder, + MetadataProvider = metadataProvider, + MetadataDisplayInfoProvider = displayInfoProvider + }); + + scriptFile = new ScriptFile(); + scriptFile.ClientFilePath = textDocument.TextDocument.Uri; + } + public static ConnectionDetails GetTestConnectionDetails() { return new ConnectionDetails diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json index 3d4d2623..06a46957 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json @@ -9,7 +9,7 @@ "System.Runtime.Serialization.Primitives": "4.1.1", "System.Data.Common": "4.1.0", "System.Data.SqlClient": "4.1.0", - "Microsoft.SqlServer.Smo": "140.1.2", + "Microsoft.SqlServer.Smo": "140.1.5", "System.Security.SecureString": "4.0.0", "System.Collections.Specialized": "4.0.1", "System.ComponentModel.TypeConverter": "4.1.0",