Fix OE and Binding Queue reliability bugs (#702)

This commit is contained in:
Matt Irvine
2018-10-05 14:14:36 -07:00
committed by GitHub
parent 6f69f7e303
commit f45155aa4a
11 changed files with 200 additions and 94 deletions

View File

@@ -39,6 +39,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary> /// </summary>
internal Dictionary<string, IBindingContext> BindingContextMap { get; set; } internal Dictionary<string, IBindingContext> BindingContextMap { get; set; }
internal Dictionary<IBindingContext, Task> BindingContextTasks { get; set; } = new Dictionary<IBindingContext, Task>();
/// <summary> /// <summary>
/// Constructor for a binding queue instance /// Constructor for a binding queue instance
/// </summary> /// </summary>
@@ -145,7 +147,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
if (!this.BindingContextMap.ContainsKey(key)) if (!this.BindingContextMap.ContainsKey(key))
{ {
this.BindingContextMap.Add(key, new T()); var bindingContext = new T();
this.BindingContextMap.Add(key, bindingContext);
this.BindingContextTasks.Add(bindingContext, Task.Run(() => null));
} }
return this.BindingContextMap[key]; return this.BindingContextMap[key];
@@ -190,11 +194,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
var bindingContext = this.BindingContextMap[key]; var bindingContext = this.BindingContextMap[key];
if (bindingContext.ServerConnection != null && bindingContext.ServerConnection.IsOpen) if (bindingContext.ServerConnection != null && bindingContext.ServerConnection.IsOpen)
{ {
bindingContext.ServerConnection.Disconnect(); // Disconnecting can take some time so run it in a separate task so that it doesn't block removal
Task.Run(() =>
{
bindingContext.ServerConnection.Cancel();
bindingContext.ServerConnection.Disconnect();
});
} }
// remove key from the map // remove key from the map
this.BindingContextMap.Remove(key); this.BindingContextMap.Remove(key);
this.BindingContextTasks.Remove(bindingContext);
} }
} }
} }
@@ -286,86 +296,119 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
continue; continue;
} }
bool lockTaken = false; var bindingContextTask = this.BindingContextTasks[bindingContext];
try
{
// prefer the queue item binding item, otherwise use the context default timeout
int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
// handle the case a previous binding operation is still running // Run in the binding context task in case this task has to wait for a previous binding operation
if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0)) this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith((task) =>
{ {
queueItem.Result = queueItem.TimeoutOperation != null bool lockTaken = false;
? queueItem.TimeoutOperation(bindingContext) try
: null; {
// prefer the queue item binding item, otherwise use the context default timeout
int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
continue; // handle the case a previous binding operation is still running
} if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0))
bindingContext.BindingLock.Reset();
lockTaken = true;
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{ {
result = queueItem.BindOperation( try
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{ {
result = queueItem.ErrorHandler(ex); Logger.Write(TraceEventType.Warning, "Binding queue operation timed out waiting for previous operation to finish");
queueItem.Result = queueItem.TimeoutOperation != null
? queueItem.TimeoutOperation(bindingContext)
: null;
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Exception running binding queue lock timeout handler: " + ex.ToString());
}
finally
{
queueItem.ItemProcessed.Set();
} }
}
});
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeout))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback return;
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
} }
lockTaken = false; bindingContext.BindingLock.Reset();
bindTask lockTaken = true;
.ContinueWith((a) => bindingContext.BindingLock.Set())
.ContinueWithOnFaulted(t => Logger.Write(TraceEventType.Error, "Binding queue threw exception " + t.Exception.ToString())); // execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
result = queueItem.ErrorHandler(ex);
}
}
});
Task.Run(() =>
{
try
{
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeout))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
bindTask.ContinueWithOnFaulted(t => Logger.Write(TraceEventType.Error, "Binding queue threw exception " + t.Exception.ToString()));
// Give the task a chance to cancel before moving on to the next operation
Task.WaitAny(bindTask, Task.Delay(bindingContext.BindingTimeout));
}
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Binding queue task completion threw exception " + ex.ToString());
}
finally
{
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
});
} }
} catch (Exception ex)
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Write(TraceEventType.Error, "Binding queue threw exception " + ex.ToString());
}
finally
{
if (lockTaken)
{ {
bindingContext.BindingLock.Set(); // catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Write(TraceEventType.Error, "Binding queue threw exception " + ex.ToString());
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
} }
}, TaskContinuationOptions.None);
queueItem.ItemProcessed.Set();
}
// if a queue processing cancellation was requested then exit the loop // if a queue processing cancellation was requested then exit the loop
if (token.IsCancellationRequested) if (token.IsCancellationRequested)

View File

@@ -4,6 +4,7 @@
// //
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
@@ -30,7 +31,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
/// <param name="refresh">force to refresh</param> /// <param name="refresh">force to refresh</param>
/// <param name="refresh">name of the sql object to filter</param> /// <param name="refresh">name of the sql object to filter</param>
/// <returns></returns> /// <returns></returns>
public abstract IEnumerable<TreeNode> Expand(TreeNode parent, bool refresh, string name, bool includeSystemObjects); public abstract IEnumerable<TreeNode> Expand(TreeNode parent, bool refresh, string name, bool includeSystemObjects, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// The list of filters that should be applied on the smo object list /// The list of filters that should be applied on the smo object list

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Threading;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel;
@@ -236,14 +237,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
/// Expands this node and returns its children /// Expands this node and returns its children
/// </summary> /// </summary>
/// <returns>Children as an IList. This is the raw children collection, not a copy</returns> /// <returns>Children as an IList. This is the raw children collection, not a copy</returns>
public IList<TreeNode> Expand(string name= null) public IList<TreeNode> Expand(string name, CancellationToken cancellationToken)
{ {
// TODO consider why solution explorer has separate Children and Items options // TODO consider why solution explorer has separate Children and Items options
if (children.IsInitialized) if (children.IsInitialized)
{ {
return children; return children;
} }
PopulateChildren(false, name); PopulateChildren(false, name, cancellationToken);
return children; return children;
} }
@@ -251,19 +252,19 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
/// Expands this node and returns its children /// Expands this node and returns its children
/// </summary> /// </summary>
/// <returns>Children as an IList. This is the raw children collection, not a copy</returns> /// <returns>Children as an IList. This is the raw children collection, not a copy</returns>
public IList<TreeNode> Expand() public IList<TreeNode> Expand(CancellationToken cancellationToken)
{ {
return Expand(null); return Expand(null, cancellationToken);
} }
/// <summary> /// <summary>
/// Refresh this node and returns its children /// Refresh this node and returns its children
/// </summary> /// </summary>
/// <returns>Children as an IList. This is the raw children collection, not a copy</returns> /// <returns>Children as an IList. This is the raw children collection, not a copy</returns>
public virtual IList<TreeNode> Refresh() public virtual IList<TreeNode> Refresh(CancellationToken cancellationToken)
{ {
// TODO consider why solution explorer has separate Children and Items options // TODO consider why solution explorer has separate Children and Items options
PopulateChildren(true, null); PopulateChildren(true, null, cancellationToken);
return children; return children;
} }
@@ -315,7 +316,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
return Parent as T; return Parent as T;
} }
protected virtual void PopulateChildren(bool refresh, string name = null) protected virtual void PopulateChildren(bool refresh, string name, CancellationToken cancellationToken)
{ {
Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "Populating oe node :{0}", this.GetNodePath())); Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "Populating oe node :{0}", this.GetNodePath()));
Debug.Assert(IsAlwaysLeaf == false); Debug.Assert(IsAlwaysLeaf == false);
@@ -337,9 +338,10 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
{ {
foreach (var factory in childFactories) foreach (var factory in childFactories)
{ {
cancellationToken.ThrowIfCancellationRequested();
try try
{ {
IEnumerable<TreeNode> items = factory.Expand(this, refresh, name, includeSystemObjects); IEnumerable<TreeNode> items = factory.Expand(this, refresh, name, includeSystemObjects, cancellationToken);
if (items != null) if (items != null)
{ {
foreach (TreeNode item in items) foreach (TreeNode item in items)

View File

@@ -418,11 +418,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{ {
if (forceRefresh) if (forceRefresh)
{ {
nodes = node.Refresh().Select(x => x.ToNodeInfo()).ToArray(); nodes = node.Refresh(cancelToken).Select(x => x.ToNodeInfo()).ToArray();
} }
else else
{ {
nodes = node.Expand().Select(x => x.ToNodeInfo()).ToArray(); nodes = node.Expand(cancelToken).Select(x => x.ToNodeInfo()).ToArray();
} }
response.Nodes = nodes; response.Nodes = nodes;
response.ErrorMessage = node.ErrorMessage; response.ErrorMessage = node.ErrorMessage;

View File

@@ -4,6 +4,7 @@
// //
using System; using System;
using System.Threading;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
@@ -60,7 +61,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{ {
return node; return node;
} }
var children = expandIfNeeded && !node.IsAlwaysLeaf ? node.Expand() : node.GetChildren(); var children = expandIfNeeded && !node.IsAlwaysLeaf ? node.Expand(new CancellationToken()) : node.GetChildren();
foreach (var child in children) foreach (var child in children)
{ {
if (filter != null && filter(child)) if (filter != null && filter(child))

View File

@@ -7,6 +7,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Threading;
using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.Utility;
@@ -40,12 +41,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
} }
} }
protected override void PopulateChildren(bool refresh, string name = null) protected override void PopulateChildren(bool refresh, string name, CancellationToken cancellationToken)
{ {
SmoQueryContext context = this.GetContextAs<SmoQueryContext>(); SmoQueryContext context = this.GetContextAs<SmoQueryContext>();
if (IsAccessible(context)) if (IsAccessible(context))
{ {
base.PopulateChildren(refresh, name); base.PopulateChildren(refresh, name, cancellationToken);
} }
else else
{ {

View File

@@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.Utility;
@@ -23,7 +24,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
return null; return null;
} }
public override IEnumerable<TreeNode> Expand(TreeNode parent, bool refresh, string name, bool includeSystemObjects) public override IEnumerable<TreeNode> Expand(TreeNode parent, bool refresh, string name, bool includeSystemObjects, CancellationToken cancellationToken)
{ {
List<TreeNode> allChildren = new List<TreeNode>(); List<TreeNode> allChildren = new List<TreeNode>();
@@ -31,7 +32,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
{ {
OnExpandPopulateFoldersAndFilter(allChildren, parent, includeSystemObjects); OnExpandPopulateFoldersAndFilter(allChildren, parent, includeSystemObjects);
RemoveFoldersFromInvalidSqlServerVersions(allChildren, parent); RemoveFoldersFromInvalidSqlServerVersions(allChildren, parent);
OnExpandPopulateNonFolders(allChildren, parent, refresh, name); OnExpandPopulateNonFolders(allChildren, parent, refresh, name, cancellationToken);
OnBeginAsyncOperations(parent); OnBeginAsyncOperations(parent);
} }
catch(Exception ex) catch(Exception ex)
@@ -83,7 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
/// </summary> /// </summary>
/// <param name="allChildren">List to which nodes should be added</param> /// <param name="allChildren">List to which nodes should be added</param>
/// <param name="parent">Parent the nodes are being added to</param> /// <param name="parent">Parent the nodes are being added to</param>
protected virtual void OnExpandPopulateNonFolders(IList<TreeNode> allChildren, TreeNode parent, bool refresh, string name) protected virtual void OnExpandPopulateNonFolders(IList<TreeNode> allChildren, TreeNode parent, bool refresh, string name, CancellationToken cancellationToken)
{ {
Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "child factory parent :{0}", parent.GetNodePath())); Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "child factory parent :{0}", parent.GetNodePath()));
@@ -115,6 +116,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
} }
foreach (var querier in queriers) foreach (var querier in queriers)
{ {
cancellationToken.ThrowIfCancellationRequested();
if (!querier.IsValidFor(serverValidFor)) if (!querier.IsValidFor(serverValidFor))
{ {
continue; continue;
@@ -125,6 +127,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
var smoObjectList = querier.Query(context, propertyFilter, refresh, smoProperties).ToList(); var smoObjectList = querier.Query(context, propertyFilter, refresh, smoProperties).ToList();
foreach (var smoObject in smoObjectList) foreach (var smoObject in smoObjectList)
{ {
cancellationToken.ThrowIfCancellationRequested();
if (smoObject == null) if (smoObject == null)
{ {
Logger.Write(TraceEventType.Error, "smoObject should not be null"); Logger.Write(TraceEventType.Error, "smoObject should not be null");

View File

@@ -8,6 +8,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer;
@@ -303,7 +304,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectExplorer
if (serverNode) if (serverNode)
{ {
Assert.Equal(nodeInfo.NodeType, NodeTypes.Server.ToString()); Assert.Equal(nodeInfo.NodeType, NodeTypes.Server.ToString());
var children = session.Root.Expand(); var children = session.Root.Expand(new CancellationToken());
//All server children should be folder nodes //All server children should be folder nodes
foreach (var item in children) foreach (var item in children)

View File

@@ -141,7 +141,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
public void QueueWithUnhandledExceptionTest() public void QueueWithUnhandledExceptionTest()
{ {
InitializeTestSettings(); InitializeTestSettings();
ManualResetEvent mre = new ManualResetEvent(false);
bool isExceptionHandled = false; bool isExceptionHandled = false;
object defaultReturnObject = new object(); object defaultReturnObject = new object();
var queueItem = this.bindingQueue.QueueBindingOperation( var queueItem = this.bindingQueue.QueueBindingOperation(
@@ -150,11 +149,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
timeoutOperation: TestTimeoutOperation, timeoutOperation: TestTimeoutOperation,
errorHandler: (exception) => { errorHandler: (exception) => {
isExceptionHandled = true; isExceptionHandled = true;
mre.Set();
return defaultReturnObject; return defaultReturnObject;
}); });
mre.WaitOne(10000); queueItem.ItemProcessed.WaitOne(10000);
this.bindingQueue.StopQueueProcessor(15000); this.bindingQueue.StopQueueProcessor(15000);
@@ -213,5 +211,58 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
Assert.Equal(1, this.timeoutCallCount); Assert.Equal(1, this.timeoutCallCount);
Assert.True(this.isCancelationRequested); Assert.True(this.isCancelationRequested);
} }
/// <summary>
/// Queue an task with a long operation causing a timeout and make sure subsequent tasks still execute
/// </summary>
[Fact]
public void QueueWithTimeoutRunsNextTask()
{
string operationKey = "testkey";
ManualResetEvent firstEventExecuted = new ManualResetEvent(false);
ManualResetEvent secondEventExecuted = new ManualResetEvent(false);
bool firstOperationCanceled = false;
bool secondOperationExecuted = false;
InitializeTestSettings();
this.bindCallbackDelay = 1000;
var totalTimeout = (this.bindCallbackDelay + this.bindingContext.BindingTimeout) * 2;
this.bindingQueue.QueueBindingOperation(
key: operationKey,
bindingTimeout: bindCallbackDelay / 2,
bindOperation: (bindingContext, cancellationToken) =>
{
secondEventExecuted.WaitOne();
if (cancellationToken.IsCancellationRequested)
{
firstOperationCanceled = true;
}
firstEventExecuted.Set();
return null;
},
timeoutOperation: TestTimeoutOperation);
this.bindingQueue.QueueBindingOperation(
key: operationKey,
bindingTimeout: bindCallbackDelay,
bindOperation: (bindingContext, cancellationToken) =>
{
secondOperationExecuted = true;
secondEventExecuted.Set();
return null;
},
waitForLockTimeout: totalTimeout
);
var result = firstEventExecuted.WaitOne(totalTimeout);
Assert.True(result);
this.bindingQueue.StopQueueProcessor(15000);
Assert.Equal(1, this.timeoutCallCount);
Assert.True(firstOperationCanceled);
Assert.True(secondOperationExecuted);
}
} }
} }

View File

@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Globalization; using System.Globalization;
using System.Threading;
using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Extensibility;
@@ -400,7 +401,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
ServerNode node = SetupServerNodeWithServer(smoServer); ServerNode node = SetupServerNodeWithServer(smoServer);
// When I populate its children // When I populate its children
IList<TreeNode> children = node.Expand(); IList<TreeNode> children = node.Expand(new CancellationToken());
// Then I expect it to contain server-level folders // Then I expect it to contain server-level folders
Assert.Equal(3, children.Count); Assert.Equal(3, children.Count);
@@ -409,7 +410,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
VerifyTreeNode<FolderNode>(children[2], "Folder", SR.SchemaHierarchy_ServerObjects); VerifyTreeNode<FolderNode>(children[2], "Folder", SR.SchemaHierarchy_ServerObjects);
// And the database is contained under it // And the database is contained under it
TreeNode databases = children[0]; TreeNode databases = children[0];
IList<TreeNode> dbChildren = databases.Expand(); IList<TreeNode> dbChildren = databases.Expand(new CancellationToken());
Assert.Equal(2, dbChildren.Count); Assert.Equal(2, dbChildren.Count);
Assert.Equal(SR.SchemaHierarchy_SystemDatabases, dbChildren[0].NodeValue); Assert.Equal(SR.SchemaHierarchy_SystemDatabases, dbChildren[0].NodeValue);

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection;
@@ -52,6 +53,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
connectedBindingContext.ServerConnection = new ServerConnection(new SqlConnection(fakeConnectionString)); connectedBindingContext.ServerConnection = new ServerConnection(new SqlConnection(fakeConnectionString));
connectedBindingQueue = new ConnectedBindingQueue(false); connectedBindingQueue = new ConnectedBindingQueue(false);
connectedBindingQueue.BindingContextMap.Add($"{details.ServerName}_{details.DatabaseName}_{details.UserName}_NULL", connectedBindingContext); connectedBindingQueue.BindingContextMap.Add($"{details.ServerName}_{details.DatabaseName}_{details.UserName}_NULL", connectedBindingContext);
connectedBindingQueue.BindingContextTasks.Add(connectedBindingContext, Task.Run(() => null));
mockConnectionOpener = new Mock<SqlConnectionOpener>(); mockConnectionOpener = new Mock<SqlConnectionOpener>();
connectedBindingQueue.SetConnectionOpener(mockConnectionOpener.Object); connectedBindingQueue.SetConnectionOpener(mockConnectionOpener.Object);
service.ConnectedBindingQueue = connectedBindingQueue; service.ConnectedBindingQueue = connectedBindingQueue;
@@ -271,7 +273,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
public void FindNodeCanExpandParentNodes() public void FindNodeCanExpandParentNodes()
{ {
var mockTreeNode = new Mock<TreeNode>(); var mockTreeNode = new Mock<TreeNode>();
object[] populateChildrenArguments = { ItExpr.Is<bool>(x => x == false), ItExpr.IsNull<string>() }; object[] populateChildrenArguments = { ItExpr.Is<bool>(x => x == false), ItExpr.IsNull<string>(), new CancellationToken() };
mockTreeNode.Protected().Setup("PopulateChildren", populateChildrenArguments); mockTreeNode.Protected().Setup("PopulateChildren", populateChildrenArguments);
mockTreeNode.Object.IsAlwaysLeaf = false; mockTreeNode.Object.IsAlwaysLeaf = false;