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}'"; } }