diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..dd02d918f7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "marquee.widgets.npm-stats.packageNames": [
+ "ptvs"
+ ]
+}
\ No newline at end of file
diff --git a/Common/Tests/Utilities.UI/TestUtilities.UI.csproj b/Common/Tests/Utilities.UI/TestUtilities.UI.csproj
index 656948c71e..c862078ff1 100644
--- a/Common/Tests/Utilities.UI/TestUtilities.UI.csproj
+++ b/Common/Tests/Utilities.UI/TestUtilities.UI.csproj
@@ -135,6 +135,10 @@
{D092D54E-FF29-4D32-9AEE-4EF704C92F67}
TestUtilities
+
+ {a52ac77f-6df9-4387-bb08-8ded3fd95a0f}
+ VSCommon
+
{3e8c485b-905a-4dbc-b059-161ccb1ad591}
TestRunnerInterop
diff --git a/Common/Tests/Utilities.UI/UI/TreeView.cs b/Common/Tests/Utilities.UI/UI/TreeView.cs
index 4d59910383..ee83c78f37 100644
--- a/Common/Tests/Utilities.UI/UI/TreeView.cs
+++ b/Common/Tests/Utilities.UI/UI/TreeView.cs
@@ -18,6 +18,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Windows.Automation;
namespace TestUtilities.UI {
@@ -92,7 +93,12 @@ public AutomationElement FindItem(params string[] path) {
protected static AutomationElement FindNode(AutomationElementCollection nodes, string[] splitPath, int depth) {
for (int i = 0; i < nodes.Count; i++) {
var node = nodes[i];
- var name = node.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
+ var name = (node.GetCurrentPropertyValue(AutomationElement.NameProperty) as string);
+
+ // Sometimes AutomationElement.NameProperty contains non-printable characters that mess up the
+ // string compare, so get rid of those.
+ // See https://stackoverflow.com/questions/40564692/c-sharp-regex-to-remove-non-printable-characters-and-control-characters-in-a
+ name = Regex.Replace(name, @"\p{C}+", string.Empty).Trim();
if (name.Equals(splitPath[depth], StringComparison.CurrentCulture)) {
if (depth == splitPath.Length - 1) {
diff --git a/Common/Tests/Utilities.UI/UI/VisualStudioApp.cs b/Common/Tests/Utilities.UI/UI/VisualStudioApp.cs
index 3591f79267..5a8bf5616b 100644
--- a/Common/Tests/Utilities.UI/UI/VisualStudioApp.cs
+++ b/Common/Tests/Utilities.UI/UI/VisualStudioApp.cs
@@ -33,6 +33,8 @@
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.Threading;
+using Microsoft.VisualStudioTools;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
using Task = System.Threading.Tasks.Task;
@@ -834,6 +836,7 @@ public virtual Project CreateProject(
private static IEnumerable EnumerateLoadedProjects(IVsSolution solution) {
IEnumHierarchies hierarchies;
Guid guid = Guid.Empty;
+
ErrorHandler.ThrowOnFailure((solution.GetProjectEnum(
(uint)(__VSENUMPROJFLAGS.EPF_ALLINSOLUTION),
ref guid,
@@ -888,45 +891,63 @@ public Project OpenProject(
bool setStartupItem = true,
Func onDialog = null
) {
+ // NOTE: In general, everything from the IVs* APIs need to run on the UI thread
+ // See the RunOnUIThread() and CreateTaskOnUIThread() helper methods
var solution = GetService(typeof(SVsSolution));
var solution4 = solution as IVsSolution4;
Assert.IsNotNull(solution, "Failed to obtain SVsSolution service");
Assert.IsNotNull(solution4, "Failed to obtain IVsSolution4 interface");
- // Close any open solution
- if (ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out string slnFile, out _))) {
- Console.WriteLine("Closing {0}", slnFile);
- solution.CloseSolutionElement(0, null, 0);
- }
+ RunOnUIThread(() =>
+ {
+ // Close any open solution
+ if (ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out string slnFile, out _)))
+ {
+ if (!string.IsNullOrEmpty(slnFile)) {
+ Console.WriteLine("Closing {0}", slnFile);
+ solution.CloseSolutionElement(0, null, 0);
+ }
+ }
+ });
string fullPath = TestData.GetPath(projName);
- if (!File.Exists(fullPath)) {
+ if (!File.Exists(fullPath))
+ {
Assert.Fail("Cannot find " + fullPath);
}
Console.WriteLine("Opening {0}", fullPath);
// If there is a .suo file, delete that so that there is no state carried over from another test.
- for (int i = 10; i <= 15; ++i) {
+ for (int i = 10; i <= 15; ++i)
+ {
string suoPath = Path.ChangeExtension(fullPath, ".v" + i + ".suo");
- if (File.Exists(suoPath)) {
+ if (File.Exists(suoPath))
+ {
File.Delete(suoPath);
}
}
var dotVsPath = Path.Combine(Path.GetDirectoryName(fullPath), ".vs");
- if (Directory.Exists(dotVsPath)) {
- foreach (var suoPath in FileUtils.EnumerateFiles(dotVsPath, ".vso")) {
+ if (Directory.Exists(dotVsPath))
+ {
+ foreach (var suoPath in FileUtils.EnumerateFiles(dotVsPath, ".vso"))
+ {
File.Delete(suoPath);
}
}
Task t;
- if (fullPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) {
- t = Task.Run(() => {
+ if (fullPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
+ {
+ t = CreateTaskOnUIThread(async () =>
+ {
ErrorHandler.ThrowOnFailure(solution.OpenSolutionFile((uint)0, fullPath));
});
- } else {
- t = Task.Run(() => {
+ }
+ else
+ {
+ t = CreateTaskOnUIThread(async () =>
+ {
Guid guidNull = Guid.Empty;
Guid iidUnknown = Guid.Empty;
IntPtr projPtr;
@@ -943,18 +964,26 @@ out projPtr
));
});
}
- using (var cts = System.Diagnostics.Debugger.IsAttached ? new CancellationTokenSource() : new CancellationTokenSource(30000)) {
- try {
- if (!t.Wait(1000, cts.Token)) {
+
+ using (var cts = System.Diagnostics.Debugger.IsAttached ? new CancellationTokenSource() : new CancellationTokenSource(30000))
+ {
+ try
+ {
+ if (!t.Wait(1000, cts.Token))
+ {
// Load has taken a while, start checking whether a dialog has
// appeared
IVsUIShell uiShell = GetService(typeof(IVsUIShell));
IntPtr hwnd;
- while (!t.Wait(1000, cts.Token)) {
+ while (!t.Wait(1000, cts.Token))
+ {
ErrorHandler.ThrowOnFailure(uiShell.GetDialogOwnerHwnd(out hwnd));
- if (hwnd != _mainWindowHandle) {
- using (var dlg = new AutomationDialog(this, AutomationElement.FromHandle(hwnd))) {
- if (onDialog == null || onDialog(dlg) == false) {
+ if (hwnd != _mainWindowHandle)
+ {
+ using (var dlg = new AutomationDialog(this, AutomationElement.FromHandle(hwnd)))
+ {
+ if (onDialog == null || onDialog(dlg) == false)
+ {
Console.WriteLine("Unexpected dialog");
DumpElement(dlg.Element);
Assert.Fail("Unexpected dialog while loading project");
@@ -963,73 +992,102 @@ out projPtr
}
}
}
- } catch (OperationCanceledException) {
+ }
+ catch (OperationCanceledException)
+ {
Assert.Fail("Failed to open project quickly enough");
}
}
- var projects = EnumerateLoadedProjects(solution).ToList();
+ IEnumerable projects = new List();
+ RunOnUIThread(() =>
+ {
+ projects = EnumerateLoadedProjects(solution).ToList();
+
+ if (expectedProjects != null && expectedProjects.Value != projects.Count())
+ {
+ // if we have other files open we can end up with a bonus project...
+ Assert.AreEqual(
+ expectedProjects,
+ projects.Count(p =>
+ {
+ string mk;
+ return ErrorHandler.Succeeded(solution.GetUniqueNameOfProject((IVsHierarchy)p, out mk)) &&
+ mk != "Miscellaneous Files";
+ }),
+ "Wrong number of loaded projects"
+ );
+ }
+ });
- if (expectedProjects != null && expectedProjects.Value != projects.Count) {
- // if we have other files open we can end up with a bonus project...
- Assert.AreEqual(
- expectedProjects,
- projects.Count(p => {
+ IVsHierarchy vsProject = null;
+ RunOnUIThread(() =>
+ {
+ vsProject = string.IsNullOrEmpty(projectName) ?
+ projects.OfType().FirstOrDefault() :
+ projects.OfType().FirstOrDefault(p =>
+ {
string mk;
- return ErrorHandler.Succeeded(solution.GetUniqueNameOfProject((IVsHierarchy)p, out mk)) &&
- mk != "Miscellaneous Files";
- }),
- "Wrong number of loaded projects"
- );
- }
-
-
- var vsProject = string.IsNullOrEmpty(projectName) ?
- projects.OfType().FirstOrDefault() :
- projects.OfType().FirstOrDefault(p => {
- string mk;
- if (ErrorHandler.Failed(solution.GetUniqueNameOfProject(p, out mk))) {
- return false;
- }
- Console.WriteLine(mk);
- return Path.GetFileNameWithoutExtension(mk) == projectName;
- });
+ if (ErrorHandler.Failed(solution.GetUniqueNameOfProject(p, out mk)))
+ {
+ return false;
+ }
+ Console.WriteLine(mk);
+ return Path.GetFileNameWithoutExtension(mk) == projectName;
+ });
+ });
string outputText = "(unable to get Solution output)";
- try {
+ try
+ {
outputText = GetOutputWindowText("Solution");
- } catch (Exception) {
+ }
+ catch (Exception)
+ {
}
Assert.IsNotNull(vsProject, "No project loaded: " + outputText);
- Guid projGuid;
- ErrorHandler.ThrowOnFailure(solution.GetGuidOfProject(vsProject, out projGuid));
+ Guid projGuid = Guid.Empty;
+ RunOnUIThread(() =>
+ {
+ ErrorHandler.ThrowOnFailure(solution.GetGuidOfProject(vsProject, out projGuid));
+ });
object o;
ErrorHandler.ThrowOnFailure(vsProject.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out o));
var project = (Project)o;
- if (project.Properties == null) {
- ErrorHandler.ThrowOnFailure(solution4.ReloadProject(ref projGuid));
+ if (project.Properties == null)
+ {
+ RunOnUIThread(() =>
+ {
+ ErrorHandler.ThrowOnFailure(solution4.ReloadProject(ref projGuid));
+ });
}
// HACK: Testing whether Properties is just slow to initialize
- for (int retries = 10; retries > 0 && project.Properties == null; --retries) {
+ for (int retries = 10; retries > 0 && project.Properties == null; --retries)
+ {
Trace.TraceWarning("Waiting for project.Properties to become non-null");
System.Threading.Thread.Sleep(250);
}
Assert.IsNotNull(project.Properties, "No project properties: " + outputText);
Assert.IsTrue(project.Properties.GetEnumerator().MoveNext(), "No items in project properties: " + outputText);
- if (startItem != null && setStartupItem) {
+ if (startItem != null && setStartupItem)
+ {
project.SetStartupFile(startItem);
- for (var i = 0; i < 20; i++) {
+ for (var i = 0; i < 20; i++)
+ {
//Wait for the startupItem to be set before returning from the project creation
- try {
- if (((string)project.Properties.Item("StartupFile").Value) == startItem) {
+ try
+ {
+ if (((string)project.Properties.Item("StartupFile").Value) == startItem)
+ {
break;
}
- } catch { }
+ }
+ catch { }
System.Threading.Thread.Sleep(250);
}
}
@@ -1120,5 +1178,37 @@ internal ProjectItem AddItem(Project project, string language, string template,
var fullTemplate = ((Solution2)project.DTE.Solution).GetProjectItemTemplate(template, language);
return project.ProjectItems.AddFromTemplate(fullTemplate, filename);
}
+
+ private static void RunOnUIThread(Action action) {
+
+ ThreadHelper.JoinableTaskFactory.Run(async () =>
+ {
+ // switch to the UI thread if we need to
+ if (!ThreadHelper.CheckAccess()) {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ }
+
+ // now that we're on the UI thread, run the specified code
+ action();
+ });
+ }
+
+ private static Task CreateTaskOnUIThread(Func task)
+ {
+ var t = ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
+ {
+ // switch to the UI thread if we need to
+ if (!ThreadHelper.CheckAccess())
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ }
+
+ // now that we're on the UI thread, run the specified code
+ await task.Invoke();
+
+ }).Task;
+
+ return t;
+ }
}
}
diff --git a/Common/Tests/Utilities.UI/UI/VisualStudioInstance.cs b/Common/Tests/Utilities.UI/UI/VisualStudioInstance.cs
index 61ccd437c7..644a8d5357 100644
--- a/Common/Tests/Utilities.UI/UI/VisualStudioInstance.cs
+++ b/Common/Tests/Utilities.UI/UI/VisualStudioInstance.cs
@@ -157,18 +157,8 @@ public string SolutionDirectory {
private string SolutionNodeText {
get {
- if (_solution.Projects.Count(sln => !sln.Flags.HasFlag(SolutionElementFlags.ExcludeFromConfiguration) && !sln.Flags.HasFlag(SolutionElementFlags.ExcludeFromSolution)) > 1) {
- return String.Format(
- "Solution '{0}' ({1} of {1} projects)",
- Path.GetFileNameWithoutExtension(_solution.Filename),
- _solution.Projects.Length
- );
- }
- return String.Format(
- "Solution '{0}' (1 of 1 project)",
- Path.GetFileNameWithoutExtension(_solution.Filename)
- );
-
+ var solutionName = Path.GetFileNameWithoutExtension(_solution.Filename);
+ return $"Solution '{solutionName}'";
}
}