From 9c6162282a47358ab924078513dae071502036dc Mon Sep 17 00:00:00 2001 From: Sharon Ravindran Date: Wed, 14 Dec 2016 16:04:47 -0800 Subject: [PATCH] Add error handling for Azure Exceptions (#177) * Add error handling for Azure Exceptions * Add SRGen for string * Add specific exception messages * Move DefinitionResult class * Add SqlLogin constant * Add error scenarios * revert timeout duration * Modify tests * Modify tests * Add tests * Revert live connection definition * Modify DefinitionsHandlerWithNoConnectionTest * fix test after merge * Code review changes * Code review changes * Code review changes --- .../Hosting/Protocol/Constants.cs | 2 + .../LanguageServices/BindingQueue.cs | 2 +- .../LanguageServices/Contracts/Definition.cs | 11 ++ .../LanguageServices/LanguageService.cs | 67 +++++-- .../LanguageServices/PeekDefinition.cs | 115 +++++++---- .../LanguageServices/PeekDefinitionResult.cs | 28 +++ .../LanguageServices/QueueItem.cs | 2 +- src/Microsoft.SqlTools.ServiceLayer/sr.cs | 62 +++++- src/Microsoft.SqlTools.ServiceLayer/sr.resx | 26 +++ .../sr.strings | 15 ++ .../LanguageServer/PeekDefinitionTests.cs | 187 ++++++++++++++---- .../Utility/TestObjects.cs | 8 +- 12 files changed, 431 insertions(+), 94 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinitionResult.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/Constants.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/Constants.cs index 14f3d762..7daa6b97 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/Constants.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/Constants.cs @@ -13,6 +13,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol public const string ContentLengthFormatString = "Content-Length: {0}\r\n\r\n"; public static readonly JsonSerializerSettings JsonSerializerSettings; + public static readonly string SqlLoginAuthenticationType = "SqlLogin"; + static Constants() { JsonSerializerSettings = new JsonSerializerSettings(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs index 07623afb..b7808509 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs @@ -57,7 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Queue a binding request item /// - public QueueItem QueueBindingOperation( + public virtual QueueItem QueueBindingOperation( string key, Func bindOperation, Func timeoutOperation = null, diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Definition.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Definition.cs index d17930e1..61ba3f82 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Definition.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Definition.cs @@ -14,5 +14,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts RequestType Type = RequestType.Create("textDocument/definition"); } + + /// + /// Error object for Definition + /// + public class DefinitionError + { + /// + /// Error message + /// + public string message { get; set; } + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index ff5b1178..b123c76c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -236,6 +236,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Store the SqlToolsContext for future use Context = context; + } #endregion @@ -317,11 +318,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ConnectionInfo connInfo; var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(textDocumentPosition.TextDocument.Uri); LanguageService.ConnectionServiceInstance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); - - Location[] locations = LanguageService.Instance.GetDefinition(textDocumentPosition, scriptFile, connInfo); - if (locations != null) - { - await requestContext.SendResult(locations); + DefinitionResult definitionResult = LanguageService.Instance.GetDefinition(textDocumentPosition, scriptFile, connInfo); + if (definitionResult != null) + { + if (definitionResult.IsErrorResult) + { + await requestContext.SendError( new DefinitionError { message = definitionResult.Message }); + } + else + { + await requestContext.SendResult(definitionResult.Locations); + } } } DocumentStatusHelper.SendStatusChange(requestContext, textDocumentPosition, DocumentStatusHelper.DefinitionRequestCompleted); @@ -688,7 +695,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// Location with the URI of the script file - internal Location[] GetDefinition(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) + internal DefinitionResult GetDefinition(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) { // Parse sql ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); @@ -726,27 +733,57 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int parserLine = textDocumentPosition.Position.Line + 1; int parserColumn = textDocumentPosition.Position.Character + 1; IEnumerable declarationItems = Resolver.FindCompletions( - scriptParseInfo.ParseResult, - parserLine, parserColumn, + scriptParseInfo.ParseResult, + parserLine, parserColumn, bindingContext.MetadataDisplayInfoProvider); // Match token with the suggestions(declaration items) returned - string schemaName = GetSchemaName(scriptParseInfo, textDocumentPosition.Position, scriptFile); - PeekDefinition peekDefinition = new PeekDefinition(bindingContext.ServerConnection); - return peekDefinition.GetScript(declarationItems, tokenText, schemaName); + + string schemaName = this.GetSchemaName(scriptParseInfo, textDocumentPosition.Position, scriptFile); + PeekDefinition peekDefinition = new PeekDefinition(bindingContext.ServerConnection, connInfo); + return peekDefinition.GetScript(declarationItems, tokenText, schemaName); + }, + timeoutOperation: (bindingContext) => + { + // return error result + return new DefinitionResult + { + IsErrorResult = true, + Message = SR.PeekDefinitionTimedoutError, + Locations = null + }; }); // wait for the queue item queueItem.ItemProcessed.WaitOne(); - return queueItem.GetResultAsT(); + return queueItem.GetResultAsT(); + } + catch (Exception ex) + { + // if any exceptions are raised return error result with message + Logger.Write(LogLevel.Error, "Exception in GetDefinition " + ex.ToString()); + return new DefinitionResult + { + IsErrorResult = true, + Message = SR.PeekDefinitionError(ex.Message), + Locations = null + }; } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } - - return null; + else + { + // User is not connected. + return new DefinitionResult + { + IsErrorResult = true, + Message = SR.PeekDefinitionNotConnectedError, + Locations = null + }; + } } /// @@ -755,7 +792,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - /// schema nama + /// schema name private string GetSchemaName(ScriptParseInfo scriptParseInfo, Position position, ScriptFile scriptFile) { // Offset index by 1 for sql parser diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinition.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinition.cs index bc0eca35..d01d1d26 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinition.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinition.cs @@ -7,12 +7,12 @@ using System.IO; using System.Collections.Generic; using System.Collections.Specialized; using System.Data.SqlClient; -using System.Runtime.InteropServices; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices @@ -23,10 +23,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// internal class PeekDefinition { + private bool error; + private string errorMessage; private ServerConnection serverConnection; - + private ConnectionInfo connectionInfo; private Database database; - private string tempPath; internal delegate StringCollection ScriptGetter(string objectName, string schemaName); @@ -42,9 +43,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// Initialize a Peek Definition helper object /// /// SMO Server connection - internal PeekDefinition(ServerConnection serverConnection) + internal PeekDefinition(ServerConnection serverConnection, ConnectionInfo connInfo) { this.serverConnection = serverConnection; + this.connectionInfo = connInfo; DirectoryInfo tempScriptDirectory = Directory.CreateDirectory(Path.GetTempPath() + "mssql_definition"); this.tempPath = tempScriptDirectory.FullName; @@ -62,20 +64,29 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices try { // Get server object from connection - SqlConnection sqlConn = new SqlConnection(this.serverConnection.ConnectionString); + SqlConnection sqlConn = new SqlConnection(this.serverConnection.ConnectionString); sqlConn.Open(); ServerConnection peekConnection = new ServerConnection(sqlConn); - Server server = new Server(peekConnection); - this.database = new Database(server, peekConnection.DatabaseName); + Server server = new Server(peekConnection); + this.database = new Database(server, peekConnection.DatabaseName); + } + catch (ConnectionFailureException cfe) + { + Logger.Write(LogLevel.Error, "Exception at PeekDefinition Database.get() : " + cfe.Message); + this.error = true; + this.errorMessage = (connectionInfo != null && connectionInfo.IsAzure)? SR.PeekDefinitionAzureError(cfe.Message) : SR.PeekDefinitionError(cfe.Message); + return null; } catch (Exception ex) { Logger.Write(LogLevel.Error, "Exception at PeekDefinition Database.get() : " + ex.Message); - } + this.error = true; + this.errorMessage = SR.PeekDefinitionError(ex.Message); + return null; + } } } - return this.database; } } @@ -114,7 +125,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { if (Path.DirectorySeparatorChar.Equals('/')) { - tempFileName = "file:" + tempFileName; + tempFileName = "file:" + tempFileName; } else { @@ -140,7 +151,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices string[] lines = script.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) { - if (lines[lineNumber].IndexOf( createString, StringComparison.OrdinalIgnoreCase) >= 0) + if (lines[lineNumber].IndexOf(createString, StringComparison.OrdinalIgnoreCase) >= 0) { return lineNumber; } @@ -155,7 +166,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// Location object of the script file - internal Location[] GetScript(IEnumerable declarationItems, string tokenText, string schemaName) + internal DefinitionResult GetScript(IEnumerable declarationItems, string tokenText, string schemaName) { foreach (Declaration declarationItem in declarationItems) { @@ -167,29 +178,38 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (declarationItem.Title.Equals(tokenText)) { // Script object using SMO based on type - DeclarationType type = declarationItem.Type; + DeclarationType type = declarationItem.Type; if (sqlScriptGetters.ContainsKey(type) && sqlObjectTypes.ContainsKey(type)) { // On *nix and mac systems, the defaultSchema property throws an Exception when accessed. // This workaround ensures that a schema name is present by attempting // to get the schema name from the declaration item // If all fails, the default schema name is assumed to be "dbo" - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && string.IsNullOrEmpty(schemaName)) + if ((connectionInfo != null && connectionInfo.ConnectionDetails.AuthenticationType.Equals(Constants.SqlLoginAuthenticationType)) && string.IsNullOrEmpty(schemaName)) { string fullObjectName = declarationItem.DatabaseQualifiedName; schemaName = this.GetSchemaFromDatabaseQualifiedName(fullObjectName, tokenText); } - return GetSqlObjectDefinition( + Location[] locations = GetSqlObjectDefinition( sqlScriptGetters[type], tokenText, schemaName, sqlObjectTypes[type] ); + DefinitionResult result = new DefinitionResult + { + IsErrorResult = this.error, + Message = this.errorMessage, + Locations = locations + }; + return result; } - return null; + // sql object type is currently not supported + return GetDefinitionErrorResult(SR.PeekDefinitionTypeNotSupportedError); } } - return null; + // no definition found + return GetDefinitionErrorResult(SR.PeekDefinitionNoResultsError); } /// @@ -203,9 +223,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices string[] tokens = fullObjectName.Split('.'); for (int i = tokens.Length - 1; i > 0; i--) { - if(tokens[i].Equals(objectName)) + if (tokens[i].Equals(objectName)) { - return tokens[i-1]; + return tokens[i - 1]; } } return "dbo"; @@ -226,7 +246,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices : new Table(this.Database, tableName, schemaName); table.Refresh(); - + return table.Script(); } catch (Exception ex) @@ -251,7 +271,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices : new View(this.Database, viewName, schemaName); view.Refresh(); - + return view.Script(); } catch (Exception ex) @@ -276,7 +296,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices : new StoredProcedure(this.Database, sprocName, schemaName); sproc.Refresh(); - + return sproc.Script(); } catch (Exception ex) @@ -300,30 +320,49 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices string schemaName, string objectType) { - StringCollection scripts = sqlScriptGetter(objectName, schemaName); - string tempFileName = (schemaName != null) ? Path.Combine(this.tempPath, string.Format("{0}.{1}.sql", schemaName, objectName)) - : Path.Combine(this.tempPath, string.Format("{0}.sql", objectName)); + StringCollection scripts = sqlScriptGetter(objectName, schemaName); + string tempFileName = (schemaName != null) ? Path.Combine(this.tempPath, string.Format("{0}.{1}.sql", schemaName, objectName)) + : Path.Combine(this.tempPath, string.Format("{0}.sql", objectName)); - if (scripts != null) + if (scripts != null) + { + int lineNumber = 0; + using (StreamWriter scriptFile = new StreamWriter(File.Open(tempFileName, FileMode.Create, FileAccess.ReadWrite))) { - int lineNumber = 0; - using (StreamWriter scriptFile = new StreamWriter(File.Open(tempFileName, FileMode.Create, FileAccess.ReadWrite))) - { - foreach (string script in scripts) + foreach (string script in scripts) + { + string createSyntax = string.Format("CREATE {0}", objectType); + if (script.IndexOf(createSyntax, StringComparison.OrdinalIgnoreCase) >= 0) { - string createSyntax = string.Format("CREATE {0}", objectType); - if (script.IndexOf(createSyntax, StringComparison.OrdinalIgnoreCase) >= 0) - { - scriptFile.WriteLine(script); - lineNumber = GetStartOfCreate(script, createSyntax); - } + scriptFile.WriteLine(script); + lineNumber = GetStartOfCreate(script, createSyntax); } } - return GetLocationFromFile(tempFileName, lineNumber); } + return GetLocationFromFile(tempFileName, lineNumber); + } + else + { + this.error = true; + this.errorMessage = SR.PeekDefinitionNoResultsError; + return null; + } + } - return null; + /// + /// Helper method to create definition error result object + /// + /// Error message + /// DefinitionResult + internal DefinitionResult GetDefinitionErrorResult(string errorMessage) + { + return new DefinitionResult + { + IsErrorResult = true, + Message = errorMessage, + Locations = null + }; } } } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinitionResult.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinitionResult.cs new file mode 100644 index 00000000..2234d6e8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/PeekDefinitionResult.cs @@ -0,0 +1,28 @@ +// +// 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.Workspace.Contracts; +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// /// Result object for PeekDefinition + /// + public class DefinitionResult + { + /// + /// True, if definition error occured + /// + public bool IsErrorResult; + + /// + /// Error message, if any + /// + public string Message { get; set; } + + /// + /// Location object representing the definition script file + /// + public Location[] Locations; + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs index a320f842..2b45de8b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs @@ -39,7 +39,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Gets or sets an event to signal when this queue item has been processed /// - public ManualResetEvent ItemProcessed { get; set; } + public virtual ManualResetEvent ItemProcessed { get; set; } /// /// Gets or sets the result of the queued task diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/sr.cs index b5d188d5..9c162533 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.cs @@ -341,6 +341,38 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string PeekDefinitionNoResultsError + { + get + { + return Keys.GetString(Keys.PeekDefinitionNoResultsError); + } + } + + public static string PeekDefinitionNotConnectedError + { + get + { + return Keys.GetString(Keys.PeekDefinitionNotConnectedError); + } + } + + public static string PeekDefinitionTimedoutError + { + get + { + return Keys.GetString(Keys.PeekDefinitionTimedoutError); + } + } + + public static string PeekDefinitionTypeNotSupportedError + { + get + { + return Keys.GetString(Keys.PeekDefinitionTypeNotSupportedError); + } + } + public static string WorkspaceServicePositionLineOutOfRange { get @@ -384,6 +416,16 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.QueryServiceQueryFailed, message); } + public static string PeekDefinitionAzureError(string errorMessage) + { + return Keys.GetString(Keys.PeekDefinitionAzureError, errorMessage); + } + + public static string PeekDefinitionError(string errorMessage) + { + return Keys.GetString(Keys.PeekDefinitionError, errorMessage); + } + public static string WorkspaceServicePositionColumnOutOfRange(int line) { return Keys.GetString(Keys.WorkspaceServicePositionColumnOutOfRange, line); @@ -397,7 +439,7 @@ namespace Microsoft.SqlTools.ServiceLayer [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Keys { - static ResourceManager resourceManager = new ResourceManager(typeof(SR)); + static ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.SR", typeof(SR).GetTypeInfo().Assembly); static CultureInfo _culture = null; @@ -540,6 +582,24 @@ namespace Microsoft.SqlTools.ServiceLayer public const string QueryServiceResultSetNoColumnSchema = "QueryServiceResultSetNoColumnSchema"; + public const string PeekDefinitionAzureError = "PeekDefinitionAzureError"; + + + public const string PeekDefinitionError = "PeekDefinitionError"; + + + public const string PeekDefinitionNoResultsError = "PeekDefinitionNoResultsError"; + + + public const string PeekDefinitionNotConnectedError = "PeekDefinitionNotConnectedError"; + + + public const string PeekDefinitionTimedoutError = "PeekDefinitionTimedoutError"; + + + public const string PeekDefinitionTypeNotSupportedError = "PeekDefinitionTypeNotSupportedError"; + + public const string WorkspaceServicePositionLineOutOfRange = "WorkspaceServicePositionLineOutOfRange"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/sr.resx index 5494a742..bf5ed353 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.resx @@ -308,6 +308,32 @@ Could not retrieve column schema for result set + + This feature is currently not supported on Azure SQL DB and Data Warehouse: {0} + . + Parameters: 0 - errorMessage (string) + + + An unexpected error occurred during Peek Definition execution: {0} + . + Parameters: 0 - errorMessage (string) + + + No results were found. + + + + Please connect to a server. + + + + Operation timed out. + + + + This object type is currently not supported by this feature. + + Position is outside of file line range diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/sr.strings index 67534aed..fee7f907 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.strings @@ -139,6 +139,21 @@ QueryServiceResultSetRowCountOutOfRange = Row count must be a positive integer QueryServiceResultSetNoColumnSchema = Could not retrieve column schema for result set +############################################################################ +# Language Service + +PeekDefinitionAzureError(string errorMessage) = This feature is currently not supported on Azure SQL DB and Data Warehouse: {0} + +PeekDefinitionError(string errorMessage) = An unexpected error occurred during Peek Definition execution: {0} + +PeekDefinitionNoResultsError = No results were found. + +PeekDefinitionNotConnectedError = Please connect to a server. + +PeekDefinitionTimedoutError = Operation timed out. + +PeekDefinitionTypeNotSupportedError = This object type is currently not supported by this feature. + ############################################################################ # Workspace Service diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/PeekDefinitionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/PeekDefinitionTests.cs index 522703fc..ac55d9cd 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/PeekDefinitionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/PeekDefinitionTests.cs @@ -3,11 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; -using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using System.Runtime.InteropServices; +using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; @@ -91,8 +94,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices requestContext = new Mock>(); requestContext.Setup(rc => rc.SendResult(It.IsAny())) .Returns(Task.FromResult(0)); - requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())); - requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())); + requestContext.Setup(rc => rc.SendError(It.IsAny())).Returns(Task.FromResult(0));; + requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0));; + requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0));; // setup the IBinder mock binder = new Mock(); @@ -103,7 +107,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices var testScriptParseInfo = new ScriptParseInfo(); LanguageService.Instance.AddOrUpdateScriptParseInfo(this.testScriptUri, testScriptParseInfo); - testScriptParseInfo.IsConnected = true; + testScriptParseInfo.IsConnected = false; testScriptParseInfo.ConnectionKey = LanguageService.Instance.BindingQueue.AddConnectionContext(connectionInfo); // setup the binding context object @@ -115,18 +119,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices /// - /// Tests the definition event handler. When called with no active connection, no definition is sent + /// Tests the definition event handler. When called with no active connection, an error is sent /// [Fact] public async Task DefinitionsHandlerWithNoConnectionTest() { TestObjects.InitializeTestServices(); InitializeTestObjects(); - // request the completion list - await Task.WhenAny(LanguageService.HandleDefinitionRequest(textDocument, requestContext.Object), Task.Delay(TaskTimeout)); - - // verify that send result was not called + // request definition + var definitionTask = await Task.WhenAny(LanguageService.HandleDefinitionRequest(textDocument, requestContext.Object), Task.Delay(TaskTimeout)); + await definitionTask; + // verify that send result was not called and send error was called requestContext.Verify(m => m.SendResult(It.IsAny()), Times.Never()); + requestContext.Verify(m => m.SendError(It.IsAny()), Times.Once()); } /// @@ -136,7 +141,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices public void GetLocationFromFileForValidFilePathTest() { String filePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\test\\script.sql" : "/test/script.sql"; - PeekDefinition peekDefinition = new PeekDefinition(null); + PeekDefinition peekDefinition = new PeekDefinition(null, null); Location[] locations = peekDefinition.GetLocationFromFile(filePath, 0); String expectedFilePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "file:///C:/test/script.sql" : "file:/test/script.sql"; @@ -149,11 +154,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetSchemaFromDatabaseQualifiedNameWithValidNameTest() { - PeekDefinition peekDefinition = new PeekDefinition(null); + PeekDefinition peekDefinition = new PeekDefinition(null, null); string validDatabaseQualifiedName = "master.test.test_table"; string objectName = "test_table"; string expectedSchemaName = "test"; - + string actualSchemaName = peekDefinition.GetSchemaFromDatabaseQualifiedName(validDatabaseQualifiedName, objectName); Assert.Equal(actualSchemaName, expectedSchemaName); } @@ -165,11 +170,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetSchemaFromDatabaseQualifiedNameWithNoSchemaTest() { - PeekDefinition peekDefinition = new PeekDefinition(null); + PeekDefinition peekDefinition = new PeekDefinition(null, null); string validDatabaseQualifiedName = "test_table"; string objectName = "test_table"; string expectedSchemaName = "dbo"; - + string actualSchemaName = peekDefinition.GetSchemaFromDatabaseQualifiedName(validDatabaseQualifiedName, objectName); Assert.Equal(actualSchemaName, expectedSchemaName); } @@ -180,15 +185,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetSchemaFromDatabaseQualifiedNameWithInvalidNameTest() { - PeekDefinition peekDefinition = new PeekDefinition(null); + PeekDefinition peekDefinition = new PeekDefinition(null, null); string validDatabaseQualifiedName = "x.y.z"; string objectName = "test_table"; string expectedSchemaName = "dbo"; - + string actualSchemaName = peekDefinition.GetSchemaFromDatabaseQualifiedName(validDatabaseQualifiedName, objectName); Assert.Equal(actualSchemaName, expectedSchemaName); } - + #if LIVE_CONNECTION_TESTS /// /// Test get definition for a table object with active connection @@ -196,9 +201,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetValidTableDefinitionTest() { - // Get live connectionInfo - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "spt_monitor"; + string schemaName = null; string objectType = "TABLE"; @@ -214,8 +223,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetTableDefinitionInvalidObjectTest() { - // Get live connectionInfo - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "test_invalid"; string schemaName = null; string objectType = "TABLE"; @@ -231,24 +243,102 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetTableDefinitionWithSchemaTest() { - // Get live connectionInfo - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "spt_monitor"; + string schemaName = "dbo"; string objectType = "TABLE"; // Get locations for valid table object with schema name Location[] locations = peekDefinition.GetSqlObjectDefinition(peekDefinition.GetTableScripts, objectName, schemaName, objectType); Assert.NotNull(locations); - Cleanup(locations); + Cleanup(locations); } /// - /// Test GetDefinition with an unsupported type(function) + /// Test GetDefinition with an unsupported type(schema - dbo). Expect a error result. /// [Fact] - public void GetUnsupportedDefinitionForFullScript() + public void GetUnsupportedDefinitionErrorTest() { + ScriptFile scriptFile; + TextDocumentPosition textDocument = new TextDocumentPosition + { + TextDocument = new TextDocumentIdentifier { Uri = OwnerUri }, + Position = new Position + { + Line = 0, + // test for 'dbo' + Character = 16 + } + }; + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfo(out scriptFile); + scriptFile.Contents = "select * from dbo.func ()"; + var languageService = new LanguageService(); + ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; + languageService.ScriptParseInfoMap.Add(OwnerUri, scriptInfo); + + // When I call the language service + var result = languageService.GetDefinition(textDocument, scriptFile, connInfo); + + // Then I expect null locations and an error to be reported + Assert.NotNull(result); + Assert.True(result.IsErrorResult); + } + + /// + /// Get Definition for a object with no definition. Expect a error result + /// + [Fact] + public void GetDefinitionWithNoResultsFoundError() + { + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); + string objectName = "from"; + + List declarations = new List(); + DefinitionResult result = peekDefinition.GetScript(declarations, objectName, null); + + Assert.NotNull(result); + Assert.True(result.IsErrorResult); + Assert.Equal(SR.PeekDefinitionNoResultsError, result.Message); + } + + /// + /// Test GetDefinition with a forced timeout. Expect a error result. + /// + [Fact] + public void GetDefinitionTimeoutTest() + { + // Given a binding queue that will automatically time out + var languageService = new LanguageService(); + Mock queueMock = new Mock(); + languageService.BindingQueue = queueMock.Object; + ManualResetEvent mre = new ManualResetEvent(true); // Do not block + Mock itemMock = new Mock(); + itemMock.Setup(i => i.ItemProcessed).Returns(mre); + + DefinitionResult timeoutResult = null; + + queueMock.Setup(q => q.QueueBindingOperation( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, Func, int?, int?>( + (key, bindOperation, timeoutOperation, blah, blah2) => + { + timeoutResult = (DefinitionResult) timeoutOperation((IBindingContext)null); + itemMock.Object.Result = timeoutResult; + }) + .Returns(() => itemMock.Object); ScriptFile scriptFile; TextDocumentPosition textDocument = new TextDocumentPosition @@ -263,12 +353,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfo(out scriptFile); scriptFile.Contents = "select * from dbo.func ()"; - var languageService = LanguageService.Instance; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; languageService.ScriptParseInfoMap.Add(OwnerUri, scriptInfo); - var locations = languageService.GetDefinition(textDocument, scriptFile, connInfo); - Assert.Null(locations); + // When I call the language service + var result = languageService.GetDefinition(textDocument, scriptFile, connInfo); + + // Then I expect null locations and an error to be reported + Assert.NotNull(result); + Assert.True(result.IsErrorResult); + // Check timeout message + Assert.Equal(SR.PeekDefinitionTimedoutError, result.Message); } /// @@ -276,8 +371,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices /// [Fact] public void GetValidViewDefinitionTest() - { - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + { + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "objects"; string schemaName = "sys"; string objectType = "VIEW"; @@ -293,7 +391,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetViewDefinitionInvalidObjectTest() { - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "objects"; string schemaName = null; string objectType = "VIEW"; @@ -308,8 +410,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetStoredProcedureDefinitionTest() { - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "sp_MSrepl_startup"; + string schemaName = "dbo"; string objectType = "PROCEDURE"; @@ -324,7 +431,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetStoredProcedureDefinitionFailureTest() { - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "SP2"; string schemaName = "dbo"; string objectType = "PROCEDURE"; @@ -339,7 +450,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices [Fact] public void GetStoredProcedureDefinitionWithoutSchemaTest() { - PeekDefinition peekDefinition = new PeekDefinition(TestObjects.InitLiveConnectionInfoForDefinition()); + // Get live connectionInfo and serverConnection + ConnectionInfo connInfo = TestObjects.InitLiveConnectionInfoForDefinition(); + ServerConnection serverConnection = TestObjects.InitLiveServerConnectionForDefinition(connInfo); + + PeekDefinition peekDefinition = new PeekDefinition(serverConnection, connInfo); string objectName = "sp_MSrepl_startup"; string schemaName = null; string objectType = "PROCEDURE"; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs index 56753010..5be7a56a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs @@ -194,7 +194,7 @@ namespace Microsoft.SqlTools.Test.Utility return connInfo; } - public static ServerConnection InitLiveConnectionInfoForDefinition() + public static ConnectionInfo InitLiveConnectionInfoForDefinition() { TestObjects.InitializeTestServices(); @@ -212,7 +212,11 @@ namespace Microsoft.SqlTools.Test.Utility ConnectionInfo connInfo = null; connectionService.TryFindConnection(ownerUri, out connInfo); - + return connInfo; + } + + public static ServerConnection InitLiveServerConnectionForDefinition(ConnectionInfo connInfo) + { SqlConnection sqlConn = new SqlConnection(ConnectionService.BuildConnectionString(connInfo.ConnectionDetails)); return new ServerConnection(sqlConn); }