diff --git a/src/DocumentationBrowserViewExtension/DocumentationBrowserView.xaml.cs b/src/DocumentationBrowserViewExtension/DocumentationBrowserView.xaml.cs index c709813bbbc..d586883027b 100644 --- a/src/DocumentationBrowserViewExtension/DocumentationBrowserView.xaml.cs +++ b/src/DocumentationBrowserViewExtension/DocumentationBrowserView.xaml.cs @@ -164,7 +164,7 @@ async void InitializeAsync() //This indicates in which location will be created the WebView2 cache folder documentationBrowser.CreationProperties = new CoreWebView2CreationProperties() { - UserDataFolder = WebBrowserUserDataFolder + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(DocumentationBrowserView)) : WebBrowserUserDataFolder }; } diff --git a/src/DynamoCore/Core/IDSDKManager.cs b/src/DynamoCore/Core/IDSDKManager.cs index 533b0768cc2..52666abe369 100644 --- a/src/DynamoCore/Core/IDSDKManager.cs +++ b/src/DynamoCore/Core/IDSDKManager.cs @@ -23,6 +23,20 @@ public class IDSDKManager : IOAuth2AuthProvider, IOAuth2AccessTokenProvider, IOA /// public event Action LoginStateChanged; + /// + /// The event is fired when IDSDK initialization fails + /// + internal event EventHandler ErrorInitializingIDSDK; + + /// + /// The flag is used to prevent multiple error messages from being shown, since initialization error can be thrown multiple times. + /// + internal bool isErrorInitializingMsgShown; + private void OnErrorInitializingIDSDK() + { + ErrorInitializingIDSDK?.Invoke(this, EventArgs.Empty); + } + /// /// Returns the login status of the current session. /// @@ -228,38 +242,41 @@ private bool Initialize() { try { - if (Client.IsInitialized()) return true; - idsdk_status_code bRet = Client.Init(); + if (Client.IsInitialized()) + { + isErrorInitializingMsgShown = false; + return true; + } - if (Client.IsSuccess(bRet)) + idsdk_status_code bRet = Client.Init(); + if (Client.IsSuccess(bRet) && Client.IsInitialized()) { - if (Client.IsInitialized()) + IntPtr hWnd = Process.GetCurrentProcess().MainWindowHandle; + if (hWnd != null) { - IntPtr hWnd = Process.GetCurrentProcess().MainWindowHandle; - if (hWnd != null) - { - Client.SetHost(hWnd); - } + Client.SetHost(hWnd); + } - bool ret = GetClientIDAndServer(out idsdk_server server, out string client_id); - if (ret) - { - Client.LogoutCompleteEvent += AuthCompleteEventHandler; - Client.LoginCompleteEvent += AuthCompleteEventHandler; - ret = SetProductConfigs(Configurations.DynamoAsString, server, client_id); - Client.SetServer(server); - return ret; - } + bool ret = GetClientIDAndServer(out idsdk_server server, out string client_id); + if (ret) + { + Client.LogoutCompleteEvent += AuthCompleteEventHandler; + Client.LoginCompleteEvent += AuthCompleteEventHandler; + ret = SetProductConfigs(Configurations.DynamoAsString, server, client_id); + Client.SetServer(server); + isErrorInitializingMsgShown = false; + return ret; } } DynamoConsoleLogger.OnLogMessageToDynamoConsole("Auth Service (IDSDK) could not be initialized!"); - return false; } catch (Exception) { DynamoConsoleLogger.OnLogMessageToDynamoConsole("An error occurred while initializing Auth Service (IDSDK)."); - return false; } + + OnErrorInitializingIDSDK(); + return false; } private bool Deinitialize() { diff --git a/src/DynamoCore/Engine/EngineController.cs b/src/DynamoCore/Engine/EngineController.cs index 00677531611..eb5ec23b71a 100644 --- a/src/DynamoCore/Engine/EngineController.cs +++ b/src/DynamoCore/Engine/EngineController.cs @@ -174,6 +174,7 @@ public void Dispose() liveRunnerServices.Dispose(); codeCompletionServices = null; + CompilationServices = null; } /// diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs index 29297a7ae0b..1e45473b1a9 100644 --- a/src/DynamoCore/Models/DynamoModel.cs +++ b/src/DynamoCore/Models/DynamoModel.cs @@ -454,6 +454,10 @@ public void ShutDown(bool shutdownHost) AnalyticsService.ShutDown(); + LuceneSearch.LuceneUtilityNodeSearch = null; + LuceneSearch.LuceneUtilityNodeAutocomplete = null; + LuceneSearch.LuceneUtilityPackageManager = null; + State = DynamoModelState.NotStarted; OnShutdownCompleted(); // Notify possible event handlers. } @@ -1588,7 +1592,6 @@ private void InitializeIncludedNodes() var cnbNode = new CodeBlockNodeSearchElement(cbnData, LibraryServices); SearchModel?.Add(cnbNode); - LuceneUtility.AddNodeTypeToSearchIndex(cnbNode, iDoc); var symbolSearchElement = new NodeModelSearchElement(symbolData) { @@ -1607,10 +1610,8 @@ private void InitializeIncludedNodes() }; SearchModel?.Add(symbolSearchElement); - LuceneUtility.AddNodeTypeToSearchIndex(symbolSearchElement, iDoc); - SearchModel?.Add(outputSearchElement); - LuceneUtility.AddNodeTypeToSearchIndex(outputSearchElement, iDoc); + LuceneUtility.AddNodeTypeToSearchIndexBulk([cnbNode, symbolSearchElement, outputSearchElement], iDoc); } @@ -1751,6 +1752,7 @@ internal void LoadNodeLibrary(Assembly assem, bool suppressZeroTouchLibraryLoad private void LoadNodeModels(List nodes, bool isPackageMember) { var iDoc = LuceneUtility.InitializeIndexDocumentForNodes(); + List nodeSearchElements = []; foreach (var type in nodes) { // Protect ourselves from exceptions thrown by malformed third party nodes. @@ -1765,7 +1767,7 @@ private void LoadNodeModels(List nodes, bool isPackageMember) // TODO: get search element some other way if (ele != null) { - LuceneUtility.AddNodeTypeToSearchIndex(ele, iDoc); + nodeSearchElements.Add(ele); } } catch (Exception e) @@ -1773,6 +1775,7 @@ private void LoadNodeModels(List nodes, bool isPackageMember) Logger.Log(e); } } + LuceneUtility.AddNodeTypeToSearchIndexBulk(nodeSearchElements, iDoc); } private void InitializePreferences() @@ -3474,26 +3477,20 @@ internal void HideUnhideNamespace(bool hide, string library, string namespc) internal void AddZeroTouchNodesToSearch(IEnumerable functionGroups) { var iDoc = LuceneUtility.InitializeIndexDocumentForNodes(); + List nodes = new(); foreach (var funcGroup in functionGroups) - AddZeroTouchNodeToSearch(funcGroup, iDoc); - } - - private void AddZeroTouchNodeToSearch(FunctionGroup funcGroup, Document iDoc) - { - foreach (var functionDescriptor in funcGroup.Functions) - { - AddZeroTouchNodeToSearch(functionDescriptor, iDoc); - } - } - - private void AddZeroTouchNodeToSearch(FunctionDescriptor functionDescriptor, Document iDoc) - { - if (functionDescriptor.IsVisibleInLibrary) { - var ele = new ZeroTouchSearchElement(functionDescriptor); - SearchModel?.Add(ele); - LuceneUtility.AddNodeTypeToSearchIndex(ele, iDoc); + foreach (var functionDescriptor in funcGroup.Functions) + { + if (functionDescriptor.IsVisibleInLibrary) + { + var ele = new ZeroTouchSearchElement(functionDescriptor); + SearchModel?.Add(ele); + nodes.Add(ele); + } + } } + LuceneUtility.AddNodeTypeToSearchIndexBulk(nodes, iDoc); } /// diff --git a/src/DynamoCore/Properties/Resources.Designer.cs b/src/DynamoCore/Properties/Resources.Designer.cs index 7935c0f1ab3..3b8e9a6526d 100644 --- a/src/DynamoCore/Properties/Resources.Designer.cs +++ b/src/DynamoCore/Properties/Resources.Designer.cs @@ -1029,7 +1029,7 @@ public static string InputPortAlternativeName { } /// - /// Looks up a localized string similar to Insert Dynamo Definition.... + /// Looks up a localized string similar to Insert Dynamo Graph.... /// public static string InsertDialogBoxText { get { diff --git a/src/DynamoCore/Properties/Resources.en-US.resx b/src/DynamoCore/Properties/Resources.en-US.resx index 95a14871ccd..e53fd90dbdf 100644 --- a/src/DynamoCore/Properties/Resources.en-US.resx +++ b/src/DynamoCore/Properties/Resources.en-US.resx @@ -900,7 +900,7 @@ This package likely contains an assembly that is blocked. You will need to load Failed to insert the file as some of the nodes already exist in the current worspace. - Insert Dynamo Definition... + Insert Dynamo Graph... Example file added to workspace. Run mode changed to Manual. diff --git a/src/DynamoCore/Properties/Resources.resx b/src/DynamoCore/Properties/Resources.resx index bef1c049ee2..7878e818089 100644 --- a/src/DynamoCore/Properties/Resources.resx +++ b/src/DynamoCore/Properties/Resources.resx @@ -903,7 +903,7 @@ This package likely contains an assembly that is blocked. You will need to load Failed to insert the file as some of the nodes already exist in the current worspace. - Insert Dynamo Definition... + Insert Dynamo Graph... Example file added to workspace. Run mode changed to Manual. diff --git a/src/DynamoCore/Search/NodeSearchModel.cs b/src/DynamoCore/Search/NodeSearchModel.cs index 34b11c008a9..6e6e90175c7 100644 --- a/src/DynamoCore/Search/NodeSearchModel.cs +++ b/src/DynamoCore/Search/NodeSearchModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Xml; using Dynamo.Configuration; using Dynamo.Graph.Nodes; @@ -10,7 +11,6 @@ using Dynamo.Utilities; using DynamoUtilities; using Lucene.Net.Documents; -using Lucene.Net.Index; using Lucene.Net.QueryParsers.Classic; using Lucene.Net.Search; @@ -231,17 +231,28 @@ internal string ProcessNodeCategory(string category, ref SearchElementGroup grou return category.Substring(0, index); } - internal IEnumerable Search(string search, LuceneSearchUtility luceneSearchUtility) + /// + /// Search for nodes by using a search key. + /// + /// + /// + /// Cancellation token to short circuit the search. + /// + /// + internal IEnumerable Search(string search, LuceneSearchUtility luceneSearchUtility, CancellationToken ctk = default) { - + ctk.ThrowIfCancellationRequested(); + if (luceneSearchUtility != null) { - //The DirectoryReader and IndexSearcher have to be assigned after commiting indexing changes and before executing the Searcher.Search() method, otherwise new indexed info won't be reflected - luceneSearchUtility.dirReader = luceneSearchUtility.writer != null ? luceneSearchUtility.writer.GetReader(applyAllDeletes: true) : DirectoryReader.Open(luceneSearchUtility.indexDir); - luceneSearchUtility.Searcher = new IndexSearcher(luceneSearchUtility.dirReader); + if (luceneSearchUtility.Searcher == null) + { + throw new Exception("Invalid IndexSearcher found"); + } string searchTerm = search.Trim(); var candidates = new List(); + var parser = new MultiFieldQueryParser(LuceneConfig.LuceneNetVersion, LuceneConfig.NodeIndexFields, luceneSearchUtility.Analyzer) { AllowLeadingWildcard = true, @@ -249,11 +260,13 @@ internal IEnumerable Search(string search, LuceneSearchUtilit FuzzyMinSim = LuceneConfig.MinimumSimilarity }; - Query query = parser.Parse(luceneSearchUtility.CreateSearchQuery(LuceneConfig.NodeIndexFields, searchTerm)); + Query query = parser.Parse(luceneSearchUtility.CreateSearchQuery(LuceneConfig.NodeIndexFields, searchTerm, false, ctk)); TopDocs topDocs = luceneSearchUtility.Searcher.Search(query, n: LuceneConfig.DefaultResultsCount); for (int i = 0; i < topDocs.ScoreDocs.Length; i++) { + ctk.ThrowIfCancellationRequested(); + // read back a Lucene doc from results Document resultDoc = luceneSearchUtility.Searcher.Doc(topDocs.ScoreDocs[i].Doc); @@ -268,7 +281,7 @@ internal IEnumerable Search(string search, LuceneSearchUtilit } else { - var foundNode = FindModelForNodeNameAndCategory(name, cat, parameters); + var foundNode = FindModelForNodeNameAndCategory(name, cat, parameters, ctk); if (foundNode != null) { candidates.Add(foundNode); @@ -280,8 +293,18 @@ internal IEnumerable Search(string search, LuceneSearchUtilit return null; } - internal NodeSearchElement FindModelForNodeNameAndCategory(string nodeName, string nodeCategory, string parameters) + /// + /// Finds the node model that corresponds to the input nodeName, nodeCategory and parameters. + /// + /// + /// + /// + /// Cancellation token to short circuit the operation. + /// + internal NodeSearchElement FindModelForNodeNameAndCategory(string nodeName, string nodeCategory, string parameters, CancellationToken ctk = default) { + ctk.ThrowIfCancellationRequested(); + var result = Entries.Where(e => { if (e.Name.Replace(" ", string.Empty).Equals(nodeName) && e.FullCategoryName.Equals(nodeCategory)) { diff --git a/src/DynamoCore/Utilities/LuceneSearchUtility.cs b/src/DynamoCore/Utilities/LuceneSearchUtility.cs index 953e76e4e91..93ba6338816 100644 --- a/src/DynamoCore/Utilities/LuceneSearchUtility.cs +++ b/src/DynamoCore/Utilities/LuceneSearchUtility.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using Dynamo.Configuration; using Dynamo.Models; using Dynamo.Search.SearchElements; @@ -23,7 +24,6 @@ using Lucene.Net.Search; using Lucene.Net.Store; using Lucene.Net.Util; -using Newtonsoft.Json; namespace Dynamo.Utilities { @@ -50,7 +50,7 @@ internal class LuceneSearchUtility internal Lucene.Net.Store.Directory indexDir; /// - /// Lucene Index write + /// Lucene Index write. Make sure to call InitializeIndexSearcher after every index change. /// internal IndexWriter writer; @@ -190,6 +190,16 @@ internal void CreateLuceneIndexWriter(OpenMode mode = OpenMode.CREATE) } } + /// + /// InitializeIndexSearcher initializes the dirReader and Searcher of this class. + /// This method should be called after every index change. + /// + private void InitializeIndexSearcher() + { + dirReader = writer != null ? writer.GetReader(applyAllDeletes: true) : DirectoryReader.Open(indexDir); + Searcher = new(dirReader); + } + /// /// Initialize Lucene index document object for reuse /// @@ -252,11 +262,8 @@ internal void UpdateIndexedNodesInfo(List nodeList) if(nodeList.Any()) { writer.DeleteAll(); - foreach(var node in nodeList) - { - var iDoc = InitializeIndexDocumentForNodes(); - AddNodeTypeToSearchIndex(node, iDoc); - } + var iDoc = InitializeIndexDocumentForNodes(); + AddNodeTypeToSearchIndexBulk(nodeList, iDoc); } } @@ -321,8 +328,9 @@ internal void SetDocumentFieldValue(Document doc, string field, string value, bo /// All fields to be searched in. /// Search key to be searched for. /// Set this to true if the search context is packages instead of nodes. + /// Cancellation token to short circuit the search. /// - internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPackageContext = false) + internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPackageContext = false, CancellationToken ctk = default) { //By Default the search will be normal SearchType searchType = SearchType.Normal; @@ -353,6 +361,7 @@ internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPac foreach (string f in fields) { Occur occurQuery = Occur.SHOULD; + ctk.ThrowIfCancellationRequested(); searchTerm = QueryParser.Escape(SearchTerm); if (searchType == SearchType.ByDotCategory) @@ -383,7 +392,7 @@ internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPac continue; //Adds the FuzzyQuery and 4 WildcardQueries (3 of them contain regular expressions), with the normal weights - AddQueries(searchTerm, f, searchType, booleanQuery, occurQuery, fuzzyLogicMaxEdits); + AddQueries(searchTerm, f, searchType, booleanQuery, occurQuery, fuzzyLogicMaxEdits, ctk); if (searchType == SearchType.ByEmptySpace) { @@ -396,7 +405,7 @@ internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPac if (string.IsNullOrEmpty(s)) continue; //Adds the FuzzyQuery and 4 WildcardQueries (3 of them contain regular expressions), with the weights for Queries with RegularExpressions - AddQueries(s, f, searchType, booleanQuery, occurQuery, LuceneConfig.FuzzySearchMinEdits, true); + AddQueries(s, f, searchType, booleanQuery, occurQuery, LuceneConfig.FuzzySearchMinEdits, ctk, true); } } } @@ -412,9 +421,12 @@ internal string CreateSearchQuery(string[] fields, string SearchTerm, bool IsPac /// The Boolean query in which the Wildcard queries will be added /// Occur type can be Should or Must /// Max edit lenght for Fuzzy queries + /// Cancellation token to short circuit the operation. /// Indicates if the SearchTerm has been split by empty space or not - private void AddQueries(string searchTerm, string field, SearchType searchType, BooleanQuery booleanQuery, Occur occurQuery, int fuzzyLogicMaxEdits, bool termSplit = false) + private void AddQueries(string searchTerm, string field, SearchType searchType, BooleanQuery booleanQuery, Occur occurQuery, int fuzzyLogicMaxEdits, CancellationToken ctk = default, bool termSplit = false) { + ctk.ThrowIfCancellationRequested(); + string querySearchTerm = searchTerm.Replace(" ", string.Empty); FuzzyQuery fuzzyQuery; @@ -582,6 +594,8 @@ internal void CommitWriterChanges() { //Commit the info indexed if index writer exists writer?.Commit(); + + InitializeIndexSearcher(); } /// @@ -589,7 +603,7 @@ internal void CommitWriterChanges() /// /// node info that will be indexed /// Lucene document in which the node info will be indexed - internal void AddNodeTypeToSearchIndex(NodeSearchElement node, Document doc) + internal void AddNodeTypeToSearchIndex_uninitialized(NodeSearchElement node, Document doc) { if (addedFields == null) return; // During DynamoModel initialization, the index writer should still be valid here @@ -628,6 +642,19 @@ internal void AddNodeTypeToSearchIndex(NodeSearchElement node, Document doc) writer?.AddDocument(doc); } + internal void AddNodeTypeToSearchIndex(NodeSearchElement node, Document doc) + { + AddNodeTypeToSearchIndex_uninitialized(node,doc); + InitializeIndexSearcher(); + } + internal void AddNodeTypeToSearchIndexBulk(List nodes, Document doc) + { + foreach(var node in nodes) + { + AddNodeTypeToSearchIndex_uninitialized(node, doc); + } + InitializeIndexSearcher(); + } } /// diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj index 57b5aef7a84..25a013533bf 100644 --- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj +++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj @@ -432,6 +432,7 @@ + diff --git a/src/DynamoCoreWpf/Properties/Resources.Designer.cs b/src/DynamoCoreWpf/Properties/Resources.Designer.cs index 2aea4a65d07..12ae85a788d 100644 --- a/src/DynamoCoreWpf/Properties/Resources.Designer.cs +++ b/src/DynamoCoreWpf/Properties/Resources.Designer.cs @@ -3731,6 +3731,25 @@ public static string HideWiresPopupMenuItem { } } + /// + /// Looks up a localized string similar to It seems like Autodesk Identity Manager is not set up on your system. To ensure full access to all features, please sign in to your Autodesk account using Autodesk Identity Manager. + ///#Download and install=https://manage.autodesk.com/products/updates it here or through Autodesk Access, then sign in.. + /// + public static string IDSDKErrorMessage { + get { + return ResourceManager.GetString("IDSDKErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Download Autodesk Identity Manager. + /// + public static string IDSDKErrorMessageTitle { + get { + return ResourceManager.GetString("IDSDKErrorMessageTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import Library. /// diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx index 2335adba2ba..308a3856e7f 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx @@ -4105,4 +4105,11 @@ To make this file into a new template, save it to a different folder, then move The compatibility of this version with your setup has not been verified. It may or may not work as expected. + + It seems like Autodesk Identity Manager is not set up on your system. To ensure full access to all features, please sign in to your Autodesk account using Autodesk Identity Manager. +#Download and install=https://manage.autodesk.com/products/updates it here or through Autodesk Access, then sign in. + + + Download Autodesk Identity Manager + \ No newline at end of file diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx index 41f89c7c712..32677073e41 100644 --- a/src/DynamoCoreWpf/Properties/Resources.resx +++ b/src/DynamoCoreWpf/Properties/Resources.resx @@ -4092,4 +4092,11 @@ To make this file into a new template, save it to a different folder, then move The compatibility of this version with your setup has not been verified. It may or may not work as expected. + + It seems like Autodesk Identity Manager is not set up on your system. To ensure full access to all features, please sign in to your Autodesk account using Autodesk Identity Manager. +#Download and install=https://manage.autodesk.com/products/updates it here or through Autodesk Access, then sign in. + + + Download Autodesk Identity Manager + \ No newline at end of file diff --git a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt index 916e9e92b74..937c6acc708 100644 --- a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt +++ b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt @@ -4815,6 +4815,8 @@ static Dynamo.Wpf.Properties.Resources.GroupStylesCancelButtonText.get -> string static Dynamo.Wpf.Properties.Resources.GroupStylesSaveButtonText.get -> string static Dynamo.Wpf.Properties.Resources.HideClassicNodeLibrary.get -> string static Dynamo.Wpf.Properties.Resources.HideWiresPopupMenuItem.get -> string +static Dynamo.Wpf.Properties.Resources.IDSDKErrorMessage.get -> string +static Dynamo.Wpf.Properties.Resources.IDSDKErrorMessageTitle.get -> string static Dynamo.Wpf.Properties.Resources.ImportLibraryDialogTitle.get -> string static Dynamo.Wpf.Properties.Resources.ImportPreferencesInfo.get -> string static Dynamo.Wpf.Properties.Resources.ImportPreferencesText.get -> string diff --git a/src/DynamoCoreWpf/UI/GuidedTour/CustomRichTextBox.cs b/src/DynamoCoreWpf/UI/GuidedTour/CustomRichTextBox.cs index 1b2e28cec27..ad6ff9dee56 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/CustomRichTextBox.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/CustomRichTextBox.cs @@ -130,7 +130,10 @@ private static FlowDocument GetCustomDocument(string Text) } //The hyperlink name is the next word followed by the # char (empty spaces are allowed) and the URL value is the one followed after the = char else + { hyperlinkName += word.Replace("#", "") + " "; + continue; + } } else if (bBoldActive) { diff --git a/src/DynamoCoreWpf/UI/SharedResourceDictionary.cs b/src/DynamoCoreWpf/UI/SharedResourceDictionary.cs index e5b2d3f0a2c..0ec805ac2bd 100644 --- a/src/DynamoCoreWpf/UI/SharedResourceDictionary.cs +++ b/src/DynamoCoreWpf/UI/SharedResourceDictionary.cs @@ -67,7 +67,6 @@ public static class SharedDictionaryManager private static ResourceDictionary outPortsDictionary; private static ResourceDictionary inPortsDictionary; private static ResourceDictionary _liveChartDictionary; - public static string ThemesDirectory { diff --git a/src/DynamoCoreWpf/Utilities/ActionDebouncer.cs b/src/DynamoCoreWpf/Utilities/ActionDebouncer.cs new file mode 100644 index 00000000000..c1c89a3e2f6 --- /dev/null +++ b/src/DynamoCoreWpf/Utilities/ActionDebouncer.cs @@ -0,0 +1,61 @@ +using Dynamo.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Dynamo.Wpf.Utilities +{ + /// + /// The ActionDebouncer class offers a means to reduce the number of UI notifications for a specified time. + /// It is meant to be used in UI elements where too many UI updates can cause perfomance issues. + /// + internal class ActionDebouncer(ILogger logger) : IDisposable + { + private readonly ILogger logger = logger; + private CancellationTokenSource cts; + + public void Cancel() + { + if (cts != null) + { + cts.Cancel(); + cts.Dispose(); + cts = null; + } + } + + /// + /// Delays the "action" for a "timeout" number of milliseconds + /// The input Action will run on same syncronization context as the Debounce method call. + /// + /// Number of milliseconds to wait + /// The action to execute after the timeout runs out. + /// A task that finishes when the deboucing is cancelled or the input action has completed (successfully or not). Should be discarded in most scenarios. + public void Debounce(int timeout, Action action) + { + Cancel(); + cts = new CancellationTokenSource(); + + Task.Delay(timeout, cts.Token).ContinueWith((t) => + { + try + { + if (t.Status == TaskStatus.RanToCompletion) + { + action(); + } + } + catch (Exception ex) + { + logger?.Log("Failed to run debounce action with the following error:"); + logger?.Log(ex.ToString()); + } + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public void Dispose() + { + Cancel(); + } + } +} diff --git a/src/DynamoCoreWpf/Utilities/ResourceUtilities.cs b/src/DynamoCoreWpf/Utilities/ResourceUtilities.cs index 1d0462aaa02..26bc287a443 100644 --- a/src/DynamoCoreWpf/Utilities/ResourceUtilities.cs +++ b/src/DynamoCoreWpf/Utilities/ResourceUtilities.cs @@ -1,11 +1,12 @@ using Dynamo.Logging; +using Dynamo.Models; using Dynamo.Wpf.Properties; using Dynamo.Wpf.UI.GuidedTour; using Dynamo.Wpf.Utilities; +using DynamoUtilities; using Microsoft.Web.WebView2.Wpf; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -598,7 +599,7 @@ internal static async Task ExecuteJSFunction(UIElement MainWindow, objec //This indicates in which location will be created the WebView2 cache folder webBrowserComponent.CreationProperties = new CoreWebView2CreationProperties() { - UserDataFolder = userDataFolder + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(ResourceUtilities)) : userDataFolder }; } diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 0de7a0bd877..634637d2c01 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -863,6 +863,10 @@ protected DynamoViewModel(StartConfiguration startConfiguration) FileTrustViewModel = new FileTrustWarningViewModel(); MLDataPipelineExtension = model.ExtensionManager.Extensions.OfType().FirstOrDefault(); + if (Model.AuthenticationManager?.AuthProvider is IDSDKManager idsdkProvider) + { + idsdkProvider.ErrorInitializingIDSDK += OnErrorInitializingIDSDK; + } } private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) @@ -1083,6 +1087,21 @@ internal void OnNodeViewReady(object nodeView) } } + /// + /// The event handler for cases when IDSDK fails to initialize, probably because of missing Adsk Identity Manager. + /// A flag is used to show the error message only once per session. + /// + private void OnErrorInitializingIDSDK(object sender, EventArgs e) + { + if (Model.AuthenticationManager?.AuthProvider is IDSDKManager idsdkProvider) + { + if (idsdkProvider.isErrorInitializingMsgShown) return; + + DynamoMessageBox.Show(Owner, WpfResources.IDSDKErrorMessage, WpfResources.IDSDKErrorMessageTitle, true, MessageBoxButton.OK, MessageBoxImage.Information); + idsdkProvider.isErrorInitializingMsgShown = true; + } + } + #region Event handler destroy/create protected virtual void UnsubscribeAllEvents() @@ -1104,6 +1123,10 @@ protected virtual void UnsubscribeAllEvents() DynamoSelection.Instance.Selection.CollectionChanged -= SelectionOnCollectionChanged; UsageReportingManager.Instance.PropertyChanged -= CollectInfoManager_PropertyChanged; + if (Model.AuthenticationManager?.AuthProvider is IDSDKManager idsdkProvider) + { + idsdkProvider.ErrorInitializingIDSDK -= OnErrorInitializingIDSDK; + } } private void InitializeRecentFiles() diff --git a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs index 482d793048b..cc23602bb06 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs @@ -606,11 +606,6 @@ public WorkspaceViewModel(WorkspaceModel model, DynamoViewModel dynamoViewModel) foreach (NoteModel note in Model.Notes) Model_NoteAdded(note); foreach (AnnotationModel annotation in Model.Annotations) Model_AnnotationAdded(annotation); foreach (ConnectorModel connector in Model.Connectors) Connectors_ConnectorAdded(connector); - - NodeAutoCompleteSearchViewModel = new NodeAutoCompleteSearchViewModel(DynamoViewModel) - { - Visible = true - }; geoScalingViewModel = new GeometryScalingViewModel(this.DynamoViewModel); geoScalingViewModel.ScaleValue = Convert.ToInt32(Math.Log10(Model.ScaleFactor)); diff --git a/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs index 31346722efa..031134a577e 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs @@ -733,11 +733,9 @@ internal void SearchAutoCompleteCandidates(string input) LuceneSearch.LuceneUtilityNodeAutocomplete = new LuceneSearchUtility(dynamoViewModel.Model, LuceneSearchUtility.DefaultStartConfig); //Memory indexing process for Node Autocomplete (indexing just the nodes returned by the NodeAutocomplete service so we limit the scope of the query search) - foreach (var node in searchElementsCache.Select(x => x.Model)) - { - var doc = LuceneUtility.InitializeIndexDocumentForNodes(); - LuceneUtility.AddNodeTypeToSearchIndex(node, doc); - } + var doc = LuceneUtility.InitializeIndexDocumentForNodes(); + List nodeSearchElements = [.. searchElementsCache.Select(x => x.Model)]; + LuceneUtility.AddNodeTypeToSearchIndexBulk(nodeSearchElements, doc); //Write the Lucene documents to memory LuceneUtility.CommitWriterChanges(); diff --git a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs index 4526b1f3afc..0c184fce5a9 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using Dynamo.Configuration; @@ -18,7 +20,9 @@ using Dynamo.UI; using Dynamo.Utilities; using Dynamo.Wpf.Services; +using Dynamo.Wpf.Utilities; using Dynamo.Wpf.ViewModels; +using DynamoUtilities; namespace Dynamo.ViewModels { @@ -73,7 +77,15 @@ public bool BrowserVisibility set { browserVisibility = value; RaisePropertyChanged("BrowserVisibility"); } } - private string searchText; + internal int searchDelayTimeout = 150; + // Feature flags activated debouncer for the search UI. + internal ActionDebouncer searchDebouncer = null; + // Cancel token source used for the node search operations. + internal CancellationTokenSource searchCancelToken; + // Enable running Search on a thread pool thread. + private bool enableSearchThreading; + + private string searchText = string.Empty; /// /// SearchText property /// @@ -86,10 +98,28 @@ public string SearchText set { searchText = value; - OnSearchTextChanged(this, EventArgs.Empty); + RaisePropertyChanged("SearchText"); RaisePropertyChanged("BrowserRootCategories"); RaisePropertyChanged("CurrentMode"); + + // The searchText is set multiple times before the control becomes visible and interactable. + // To prevent any debounces from triggering at some unexpected point before or after the control + // becomes visible, this flag is only set once the searchText value is set by the user + // (unless it somehow gets set somewhere else) + // + // pinzart: The search text is set multiple times with an empty value. Seems sufficient to only use the debouncer + // if we get a non-empty value. + if (!string.IsNullOrEmpty(searchText) && searchDebouncer != null) + { + searchDebouncer.Debounce(searchDelayTimeout, () => OnSearchTextChanged(this, EventArgs.Empty)); + } + else + { + // Make sure any previously scheduled debounces are cancelled + searchDebouncer?.Cancel(); + OnSearchTextChanged(this, EventArgs.Empty); + } } } @@ -374,9 +404,24 @@ internal SearchViewModel(DynamoViewModel dynamoViewModel) iconServices = new IconServices(pathManager); + DynamoFeatureFlagsManager.FlagsRetrieved += TryInitializeSearchFlags; + InitializeCore(); } + private void TryInitializeSearchFlags() + { + if (DynamoModel.FeatureFlags?.CheckFeatureFlag("searchbar_debounce", false) ?? false) + { + searchDebouncer ??= new ActionDebouncer(dynamoViewModel?.Model?.Logger); + } + + if (DynamoModel.FeatureFlags?.CheckFeatureFlag("searchbar_separate_thread", false) ?? false) + { + enableSearchThreading = true; + } + } + // Just for tests. Please refer to LibraryTests.cs internal SearchViewModel(NodeSearchModel model) { @@ -407,6 +452,9 @@ public override void Dispose() Model.EntryUpdated -= UpdateEntry; Model.EntryRemoved -= RemoveEntry; + searchDebouncer?.Dispose(); + DynamoFeatureFlagsManager.FlagsRetrieved -= TryInitializeSearchFlags; + base.Dispose(); } @@ -434,6 +482,9 @@ private void InitializeCore() DefineFullCategoryNames(LibraryRootCategories, ""); InsertClassesIntoTree(LibraryRootCategories); + + // If feature flags are already cached, try to initialize the debouncer + TryInitializeSearchFlags(); } private void AddEntry(NodeSearchElement entry) @@ -871,28 +922,69 @@ internal void SearchAndUpdateResults() RaisePropertyChanged("IsAnySearchResult"); } - /// - /// Performs a search and updates searchResults. - /// - /// The search query - public void SearchAndUpdateResults(string query) + internal Task SearchAndUpdateResultsTask(string query) { if (Visible != true) - return; + return Task.CompletedTask; // if the search query is empty, go back to the default treeview if (string.IsNullOrEmpty(query)) - return; + return Task.CompletedTask; + + // A new search should cancel any existing searches. + searchCancelToken?.Cancel(); + searchCancelToken?.Dispose(); + + searchCancelToken = new(); + + // The TaskScheduler.FromCurrentSynchronizationContext() exists only if there is a valid SyncronizationContex/ + // Calling this method from a non UI thread could have a null SyncronizationContex.Current, + // so in that case we use the default TaskScheduler which uses the thread pool. + var taskScheduler = SynchronizationContext.Current != null ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Default; + + // We run the searches on the thread pool to reduce the impact on the UI thread. + return Task.Run(() => + { + return Search(query, searchCancelToken.Token); + + }, searchCancelToken.Token).ContinueWith((t, o) => + { + // This continuation will execute on the UI thread (forced by using FromCurrentSynchronizationContext()) + searchResults = new List(t.Result); - //Passing the second parameter as true will search using Lucene.NET - var foundNodes = Search(query); - searchResults = new List(foundNodes); + FilteredResults = searchResults; + UpdateSearchCategories(); + }, taskScheduler, TaskContinuationOptions.OnlyOnRanToCompletion); + } + + /// + /// Performs a search and updates searchResults. + /// + /// The search query + [Obsolete(@"This method will be removed in a future release. The internal search operation is done asyncronously, so when this method call exits, the search operation might not be done yet. + Please use the task based method SearchAndUpdateResultsTask instead.")] + public void SearchAndUpdateResults(string query) + { + if (enableSearchThreading) + { + SearchAndUpdateResultsTask(query); + } + else + { + if (Visible != true) + return; - FilteredResults = searchResults; + // if the search query is empty, go back to the default treeview + if (string.IsNullOrEmpty(query)) + return; - UpdateSearchCategories(); + //Passing the second parameter as true will search using Lucene.NET + var foundNodes = Search(query); + searchResults = new List(foundNodes); - RaisePropertyChanged("FilteredResults"); + FilteredResults = searchResults; + UpdateSearchCategories(); + } } /// @@ -932,11 +1024,12 @@ private void IsSelectedChanged(object sender, PropertyChangedEventArgs e) /// /// Returns a list with a maximum MaxNumSearchResults elements. /// The search query - internal IEnumerable Search(string search) + /// A cancellation token for this operation. + internal IEnumerable Search(string search, CancellationToken ctk = default) { if (LuceneUtility != null) { - var searchElements = Model.Search(search, LuceneUtility); + var searchElements = Model.Search(search, LuceneUtility, ctk); if (searchElements != null) { return searchElements.Select(MakeNodeSearchElementVM); diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs index bd0f4c8f381..c2751860a21 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs @@ -2081,6 +2081,7 @@ private void WindowClosed(object sender, EventArgs e) this.dynamoViewModel.RequestEnableShortcutBarItems -= DynamoViewModel_RequestEnableShortcutBarItems; this.dynamoViewModel.RequestExportWorkSpaceAsImage -= OnRequestExportWorkSpaceAsImage; this.dynamoViewModel.RequestShorcutToolbarLoaded -= onRequestShorcutToolbarLoaded; + PythonEngineManager.Instance.AvailableEngines.CollectionChanged -= OnPythonEngineListUpdated; if (homePage != null) { diff --git a/src/DynamoCoreWpf/Views/HomePage/HomePage.xaml.cs b/src/DynamoCoreWpf/Views/HomePage/HomePage.xaml.cs index 68e58344c96..a936c75817b 100644 --- a/src/DynamoCoreWpf/Views/HomePage/HomePage.xaml.cs +++ b/src/DynamoCoreWpf/Views/HomePage/HomePage.xaml.cs @@ -156,7 +156,7 @@ private async void UserControl_Loaded(object sender, System.Windows.RoutedEventA dynWebView.CreationProperties = new CoreWebView2CreationProperties { - UserDataFolder = webBrowserUserDataFolder.FullName + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(HomePage)) : webBrowserUserDataFolder.FullName }; //ContentRendered ensures that the webview2 component is visible. diff --git a/src/DynamoCoreWpf/Views/SplashScreen/SplashScreen.xaml.cs b/src/DynamoCoreWpf/Views/SplashScreen/SplashScreen.xaml.cs index 10ebfb3326a..a72562d17fa 100644 --- a/src/DynamoCoreWpf/Views/SplashScreen/SplashScreen.xaml.cs +++ b/src/DynamoCoreWpf/Views/SplashScreen/SplashScreen.xaml.cs @@ -347,7 +347,7 @@ protected override async void OnContentRendered(EventArgs e) webView.CreationProperties = new CoreWebView2CreationProperties { - UserDataFolder = webBrowserUserDataFolder.FullName + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(SplashScreen)) : webBrowserUserDataFolder.FullName }; //ContentRendered ensures that the webview2 component is visible. diff --git a/src/DynamoUtilities/TestUtilities.cs b/src/DynamoUtilities/TestUtilities.cs index 15bae8cbd41..c6819a49e87 100644 --- a/src/DynamoUtilities/TestUtilities.cs +++ b/src/DynamoUtilities/TestUtilities.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -11,5 +14,11 @@ internal static class TestUtilities { // Simple string that we can store in DynamoWebView2 instances so that we can track them down more easily internal static string WebView2Tag; + + internal static string UserDataFolderDuringTests(string appName) + { + var directory = new DirectoryInfo(Assembly.GetExecutingAssembly().Location); + return Path.Combine(directory.Parent.Parent.Parent.FullName, "test", $"webview2_{Environment.ProcessId}_{appName}_appdata"); + } } } diff --git a/src/LibraryViewExtensionWebView2/LibraryViewController.cs b/src/LibraryViewExtensionWebView2/LibraryViewController.cs index b85a8ca17a5..208b2c74eda 100644 --- a/src/LibraryViewExtensionWebView2/LibraryViewController.cs +++ b/src/LibraryViewExtensionWebView2/LibraryViewController.cs @@ -331,7 +331,7 @@ async void InitializeAsync() //This indicates in which location will be created the WebView2 cache folder this.browser.CreationProperties = new CoreWebView2CreationProperties() { - UserDataFolder = WebBrowserUserDataFolder + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(LibraryViewController)) : WebBrowserUserDataFolder }; } diff --git a/src/Notifications/NotificationCenterController.cs b/src/Notifications/NotificationCenterController.cs index 03f8b497309..66e8ce78961 100755 --- a/src/Notifications/NotificationCenterController.cs +++ b/src/Notifications/NotificationCenterController.cs @@ -114,7 +114,7 @@ private async void InitializeBrowserAsync(object sender, RoutedEventArgs e) //This indicates in which location will be created the WebView2 cache folder notificationUIPopup.webView.CreationProperties = new CoreWebView2CreationProperties() { - UserDataFolder = webBrowserUserDataFolder.FullName + UserDataFolder = DynamoModel.IsTestMode ? TestUtilities.UserDataFolderDuringTests(nameof(NotificationCenterController)) : webBrowserUserDataFolder.FullName }; } notificationUIPopup.webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted; diff --git a/src/Tools/DynamoFeatureFlags/FeatureFlagsClient.cs b/src/Tools/DynamoFeatureFlags/FeatureFlagsClient.cs index 1d618f5fcfb..925ac30669a 100644 --- a/src/Tools/DynamoFeatureFlags/FeatureFlagsClient.cs +++ b/src/Tools/DynamoFeatureFlags/FeatureFlagsClient.cs @@ -85,7 +85,11 @@ internal FeatureFlagsClient(string userkey, string mobileKey = null, bool testMo AllFlags = LdValue.ObjectFrom(new Dictionary { { "TestFlag1",LdValue.Of(true) }, { "TestFlag2", LdValue.Of("I am a string") }, //in tests we want instancing on so we can test it. - { "graphics-primitive-instancing", LdValue.Of(true) } }); + { "graphics-primitive-instancing", LdValue.Of(true) }, + //in tests we want search debouncing on so we can test it. + { "searchbar_debounce", LdValue.Of(true) }, + //in tests we want to run search on non UI thread so we can test it. + { "searchbar_separate_thread", LdValue.Of(true) }}); return; } diff --git a/test/DynamoCoreTests/SerializationTests.cs b/test/DynamoCoreTests/SerializationTests.cs index 4057c9f573b..73661ff7ad8 100644 --- a/test/DynamoCoreTests/SerializationTests.cs +++ b/test/DynamoCoreTests/SerializationTests.cs @@ -602,9 +602,10 @@ public override bool Equals(object obj) [TestFixture, Category("Serialization")] public class SerializationTests : DynamoModelTestBase { - public static string jsonNonGuidFolderName = "json_nonGuidIds"; - public static string jsonFolderName = "json"; - public static string jsonFolderNameDifferentCulture = "json_differentCulture"; + private static Dictionary testFilesCache = []; + public static string jsonNonGuidFolderName = $"json_nonGuidIds_{Environment.ProcessId}"; + public static string jsonFolderName = $"json_{Environment.ProcessId}"; + public static string jsonFolderNameDifferentCulture = $"json_differentCulture_{Environment.ProcessId}"; private const int MAXNUM_SERIALIZATIONTESTS_TOEXECUTE = 300; // Filter out dyns that change during testing @@ -660,6 +661,7 @@ public void FixtureSetup() Console.WriteLine(e.Message); } } + CacheTestFiles(); } [OneTimeTearDown] @@ -938,11 +940,26 @@ public void AllTypesSerialize() serializationTestUtils.SaveWorkspaceComparisonData); } - public static object[] FindWorkspaces() + private void CacheTestFiles() { + testFilesCache.Clear(); var di = new DirectoryInfo(TestDirectory); var fis = di.GetFiles("*.dyn", SearchOption.AllDirectories); - return fis.Where(fi => !filterOutFromSerializationTests.Contains(fi.Name)).Select(fi => fi.FullName).Take(MAXNUM_SERIALIZATIONTESTS_TOEXECUTE).ToArray(); + var testFiles = fis.Where(fi => !filterOutFromSerializationTests.Contains(fi.Name)).Select(fi => fi.FullName).Take(MAXNUM_SERIALIZATIONTESTS_TOEXECUTE).ToArray(); + + foreach (var testFile in testFiles) { + testFilesCache.Add(Guid.NewGuid().ToString(), testFile); + } + } + + internal static string GetTestFileNameFromGuid(string guid) + { + return testFilesCache.GetValueOrDefault(guid); + } + + public static object[] FindWorkspaces() + { + return [.. testFilesCache.Keys]; } /// @@ -950,11 +967,14 @@ public static object[] FindWorkspaces() /// the test directory, opens them and executes, then converts them to /// json and executes again, comparing the values from the two runs. /// - /// The path to a .dyn file. This parameter is supplied - /// by the test framework. + /// A random guid assigned to a .dyn file. This parameter is supplied + /// by the test framework. You can get the file path by calling GetTestFileNameFromGuid(fileId). [Test, TestCaseSource(nameof(FindWorkspaces)), Category("JsonTestExclude")] - public void SerializationTest(string filePath) + public void SerializationTest(string fileId) { + string filePath = GetTestFileNameFromGuid(fileId); + Console.WriteLine($"Running test {TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.MethodName} with file {filePath}"); + modelsGuidToIdMap.Clear(); DoWorkspaceOpenAndCompare(filePath, jsonFolderName, ConvertCurrentWorkspaceToJsonAndSave, serializationTestUtils.CompareWorkspaceModels, @@ -967,11 +987,14 @@ public void SerializationTest(string filePath) /// json and executes again, comparing the values from the two runs /// while being in a different culture. /// - /// The path to a .dyn file. This parameter is supplied - /// by the test framework. + /// A random guid assigned to a .dyn file. This parameter is supplied + /// by the test framework. You can get the file path by calling GetTestFileNameFromGuid(fileId). [Test, TestCaseSource(nameof(FindWorkspaces)), Category("JsonTestExclude")] - public void SerializationInDifferentCultureTest(string filePath) + public void SerializationInDifferentCultureTest(string fileId) { + string filePath = GetTestFileNameFromGuid(fileId); + Console.WriteLine($"Running test {TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.MethodName} with file {filePath}"); + var frCulture = CultureInfo.CreateSpecificCulture("fr-FR"); // Save current culture - usually "en-US" @@ -998,11 +1021,14 @@ public void SerializationInDifferentCultureTest(string filePath) /// This set of tests has slightly modified json where the id properties /// are altered when serialized to test deserialization of non-guid ids. /// - /// The path to a .dyn file. This parameter is supplied - /// by the test framework. + /// A random guid assigned to a .dyn file. This parameter is supplied + /// by the test framework. You can get the file path by calling GetTestFileNameFromGuid(fileId). [Test, TestCaseSource(nameof(FindWorkspaces)), Category("JsonTestExclude")] - public void SerializationNonGuidIdsTest(string filePath) + public void SerializationNonGuidIdsTest(string fileId) { + string filePath = GetTestFileNameFromGuid(fileId); + Console.WriteLine($"Running test {TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.MethodName} with file {filePath}"); + modelsGuidToIdMap.Clear(); DoWorkspaceOpenAndCompare(filePath, jsonNonGuidFolderName, ConvertCurrentWorkspaceToNonGuidJsonAndSave, diff --git a/test/DynamoCoreWpfTests/CoreUITests.cs b/test/DynamoCoreWpfTests/CoreUITests.cs index 7bdfeb586a4..359e8a0f86a 100644 --- a/test/DynamoCoreWpfTests/CoreUITests.cs +++ b/test/DynamoCoreWpfTests/CoreUITests.cs @@ -26,6 +26,7 @@ using Dynamo.Utilities; using Dynamo.ViewModels; using Dynamo.Views; +using Dynamo.Wpf.Utilities; using DynamoCoreWpfTests.Utility; using Moq; using Moq.Protected; @@ -968,12 +969,202 @@ public void InCanvasSearchTextChangeTriggersOneSearchCommand() int count = 0; (searchControl.DataContext as SearchViewModel).SearchCommand = new Dynamo.UI.Commands.DelegateCommand((object _) => { count++; }); searchControl.SearchTextBox.Text = "dsfdf"; - DispatcherUtil.DoEvents(); + DispatcherUtil.DoEventsLoop(() => count == 1); Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); Assert.AreEqual(count, 1); } + [Test] + [Category("UnitTests")] + public void InCanvasSearchTextChangeTriggersOneSearchCommandWithDebounce() + { + var currentWs = View.ChildOfType(); + + // open context menu + RightClick(currentWs.zoomBorder); + + // show in-canvas search + ViewModel.CurrentSpaceViewModel.ShowInCanvasSearchCommand.Execute(ShowHideFlags.Show); + + var searchControl = currentWs.ChildrenOfType().Select(x => (x as Popup)?.Child as InCanvasSearchControl).Where(c => c != null).FirstOrDefault(); + Assert.IsNotNull(searchControl); + + DispatcherUtil.DoEvents(); + + int count = 0; + var vm = searchControl.DataContext as SearchViewModel; + Assert.IsNotNull(vm); + vm.SearchCommand = new Dynamo.UI.Commands.DelegateCommand((object _) => { count++; }); + + // run without debouncer + vm.searchDebouncer.Dispose(); + vm.searchDebouncer = null; // disable the debouncer + searchControl.SearchTextBox.Text = "dsfdf"; + DispatcherUtil.DoEventsLoop(() => count == 1); + + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(1, count, "changing the text once should cause a single update"); + + int currThreadId = Environment.CurrentManagedThreadId; + int debouncerThreadId = -1; + var debouncer = new ActionDebouncer(null); + + int dbCount = 0; + debouncer.Debounce(100, () => + { + dbCount++; + debouncerThreadId = Environment.CurrentManagedThreadId; + }); + + DispatcherUtil.DoEventsLoop(() => debouncerThreadId != -1); + Assert.AreEqual(currThreadId, debouncerThreadId); + + vm.searchDebouncer = debouncer; + searchControl.SearchTextBox.Text = "dsfdf"; + DispatcherUtil.DoEventsLoop(() => dbCount == 1); + Assert.AreEqual(1, dbCount); + + // Empty strings should not hit the debounce action + searchControl.SearchTextBox.Text = ""; + Assert.AreEqual(1, dbCount); + DispatcherUtil.DoEventsLoop(() => count == 2); + } + + [Test] + [Category("UnitTests")] + public void InCanvasSearchTextWithDebouncer() + { + var currentWs = View.ChildOfType(); + + // open context menu + RightClick(currentWs.zoomBorder); + + // show in-canvas search + ViewModel.CurrentSpaceViewModel.ShowInCanvasSearchCommand.Execute(ShowHideFlags.Show); + + var searchControl = currentWs.ChildrenOfType().Select(x => (x as Popup)?.Child as InCanvasSearchControl).Where(c => c != null).FirstOrDefault(); + Assert.IsNotNull(searchControl); + + DispatcherUtil.DoEvents(); + + int count = 0; + var vm = searchControl.DataContext as SearchViewModel; + Assert.IsNotNull(vm); + + // Check that the default debouncer is setup. + Assert.IsNotNull(vm.searchDebouncer); + + void Vm_SearchTextChanged(object sender, EventArgs e) + { + count++; + throw new Exception("Failure that should be logged"); + } + + vm.searchDelayTimeout = 50; + vm.SearchTextChanged += Vm_SearchTextChanged; + + vm.SearchText = "point"; + DispatcherUtil.DoEventsLoop(() => count == 1); + Assert.AreEqual(1, count, "Search updates were sent out"); + + + vm.SearchText = "point.by"; + DispatcherUtil.DoEventsLoop(() => count == 2); + Assert.AreEqual(2, count, "thread sees updated count"); + + vm.SearchText = "abcde"; + DispatcherUtil.DoEventsLoop(() => count == 3); + Assert.AreEqual(3, count, "thread sees updated count"); + + searchControl.SearchTextBox.Text = ""; + DispatcherUtil.DoEventsLoop(() => currentWs.InCanvasSearchBar.IsOpen); + + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(4, count, "main sees updated count"); + } + + [Test] + [Category("UnitTests")] + public void InCanvasSearchTextChangeTriggersDebouncer() + { + var currentWs = View.ChildOfType(); + + // open context menu + RightClick(currentWs.zoomBorder); + + // show in-canvas search + ViewModel.CurrentSpaceViewModel.ShowInCanvasSearchCommand.Execute(ShowHideFlags.Show); + + var searchControl = currentWs.ChildrenOfType().Select(x => (x as Popup)?.Child as InCanvasSearchControl).Where(c => c != null).FirstOrDefault(); + Assert.IsNotNull(searchControl); + + DispatcherUtil.DoEvents(); + + int count = 0; + var vm = searchControl.DataContext as SearchViewModel; + Assert.IsNotNull(vm); + vm.SearchCommand = new Dynamo.UI.Commands.DelegateCommand((object _) => { count++; }); + + // prepare debounce tests + vm.searchDelayTimeout = 50; + var sleepTime = vm.searchDelayTimeout * 2; + Assert.NotNull(vm.searchDebouncer); + + // run with debouncer + count = 0; + searchControl.SearchTextBox.Text = "dsfdfdsfdf"; + Thread.Sleep(sleepTime); + DispatcherUtil.DoEventsLoop(() => count == 1); + + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(1, count, "changing the text once should cause a single update after timeout expires"); + + // multiple updates with debouncer + count = 0; + searchControl.SearchTextBox.Text = "dsfdf"; + searchControl.SearchTextBox.Text = "dsfdfdsfdf"; + searchControl.SearchTextBox.Text = "wer"; + searchControl.SearchTextBox.Text = "dsfdf"; + DispatcherUtil.DoEventsLoop(() => count == 1); + // Do another events loop to make sure no other debouncer actions are triggered + DispatcherUtil.DoEventsLoop(null, 10); + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(1, count, "changing the text multiple times in quick succession should cause a single update once timeout expires"); + + // multiple updates with empty string debouncer + count = 0; + searchControl.SearchTextBox.Text = "dsfdf"; + searchControl.SearchTextBox.Text = ""; + searchControl.SearchTextBox.Text = "dsfdf"; + DispatcherUtil.DoEventsLoop(() => count == 2); + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(2, count, "changing the text to empty should not trigger the debouncer"); + + // test timeout expiration + count = 0; + searchControl.SearchTextBox.Text = "dsfdfdsfdf"; + DispatcherUtil.DoEventsLoop(() => count == 1); + searchControl.SearchTextBox.Text = "dsfdf"; + DispatcherUtil.DoEventsLoop(() => count == 2); + + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(2, count, "2 timeout expirations should cause 2 updates"); + + // run with debouncer, then without + count = 0; + searchControl.SearchTextBox.Text = "dsfdfdsfdf"; + vm.searchDebouncer.Dispose(); + vm.searchDebouncer = null; // disable debounce + searchControl.SearchTextBox.Text = "dsfdf"; + + DispatcherUtil.DoEventsLoop(() => count == 1); + DispatcherUtil.DoEventsLoop(null, 10); + + Assert.IsTrue(currentWs.InCanvasSearchBar.IsOpen); + Assert.AreEqual(1, count, "the debounced update should have been cancelled by the immediate set"); + } + [Test] public void WarningShowsWhenSavingWithLinterWarningsOrErrors() { diff --git a/test/DynamoCoreWpfTests/DynamoTestUIBase.cs b/test/DynamoCoreWpfTests/DynamoTestUIBase.cs index 65777bb0ce3..42d90b227d3 100644 --- a/test/DynamoCoreWpfTests/DynamoTestUIBase.cs +++ b/test/DynamoCoreWpfTests/DynamoTestUIBase.cs @@ -24,6 +24,82 @@ namespace DynamoCoreWpfTests { + internal class TestDiagnostics + { + internal int DispatcherOpsCounter = 0; + // Use this flag to skip trying to execute all the dispatched operations during the test lifetime. + // This flag should only be used very sparingly + internal bool SkipDispatcherFlush = false; + + private void Hooks_OperationPosted(object sender, DispatcherHookEventArgs e) + { + e.Operation.Task.ContinueWith((t) => { + Interlocked.Decrement(ref DispatcherOpsCounter); + }, TaskScheduler.Default); + Interlocked.Increment(ref DispatcherOpsCounter); + } + + private void PrettyPrint(object obj) + { + Type type = obj.GetType(); + PropertyInfo[] properties = type.GetProperties(); + + Console.WriteLine("{"); + foreach (var property in properties) + { + try + { + Console.WriteLine($" {property.Name}: {property.GetValue(obj)}"); + } + catch (Exception e) + { + Console.WriteLine($" {property.Name}: {e.Message}"); + } + } + Console.WriteLine("}"); + } + + internal void SetupStartupDiagnostics() + { + System.Console.WriteLine($"PID {Process.GetCurrentProcess().Id} Start test: {TestContext.CurrentContext.Test.Name}"); + TestUtilities.WebView2Tag = TestContext.CurrentContext.Test.Name; + + SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext()); + + Dispatcher.CurrentDispatcher.Hooks.OperationPosted += Hooks_OperationPosted; + } + + internal void SetupBeforeCleanupDiagnostics() + { + Dispatcher.CurrentDispatcher.Hooks.OperationPosted -= Hooks_OperationPosted; + if (!SkipDispatcherFlush) + { + DispatcherUtil.DoEventsLoop(() => DispatcherOpsCounter == 0); + } + } + + internal void SetupAfterCleanupDiagnostics() + { + TestUtilities.WebView2Tag = string.Empty; + using (var currentProc = Process.GetCurrentProcess()) + { + int id = currentProc.Id; + var name = TestContext.CurrentContext.Test.Name; + System.Console.WriteLine($"PID {id} Finished test: {name} with DispatcherOpsCounter = {DispatcherOpsCounter}"); + System.Console.WriteLine($"PID {id} Finished test: {name} with WorkingSet = {currentProc.WorkingSet64}"); + System.Console.WriteLine($"PID {id} Finished test: {name} with PrivateBytes = {currentProc.PrivateMemorySize64}"); + System.Console.Write($"PID {id} Finished test: {name} with GC Memory Info: "); + PrettyPrint(GC.GetGCMemoryInfo()); + } + } + + internal void SetupCleanupDiagnostics() + { + SetupBeforeCleanupDiagnostics(); + SetupAfterCleanupDiagnostics(); + } + } + public class DynamoTestUIBase { protected Preloader preloader; @@ -34,8 +110,17 @@ public class DynamoTestUIBase // Use this flag to skip trying to execute all the dispatched operations during the test lifetime. // This flag should only be used very sparingly - protected bool SkipDispatcherFlush = false; - protected int DispatcherOpsCounter = 0; + protected bool SkipDispatcherFlush { + get => testDiagnostics.SkipDispatcherFlush; + set => testDiagnostics.SkipDispatcherFlush = value; + } + + [Obsolete("This property will be deprecated as it is for internal use only.")] + protected int DispatcherOpsCounter { + get => testDiagnostics.DispatcherOpsCounter; + } + + private TestDiagnostics testDiagnostics = new(); protected string ExecutingDirectory { @@ -47,9 +132,7 @@ protected string ExecutingDirectory [SetUp] public virtual void Start() { - System.Console.WriteLine($"PID {Process.GetCurrentProcess().Id} Start test: {TestContext.CurrentContext.Test.Name}"); - TestUtilities.WebView2Tag = TestContext.CurrentContext.Test.Name; - + testDiagnostics.SetupStartupDiagnostics(); var assemblyPath = Assembly.GetExecutingAssembly().Location; preloader = new Preloader(Path.GetDirectoryName(assemblyPath)); preloader.Preload(); @@ -87,11 +170,6 @@ public virtual void Start() //create the view View = new DynamoView(ViewModel); View.Show(); - - SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext()); - - Dispatcher.CurrentDispatcher.Hooks.OperationPosted += Hooks_OperationPosted; - } protected static void RaiseLoadedEvent(FrameworkElement element) @@ -104,14 +182,6 @@ protected static void RaiseLoadedEvent(FrameworkElement element) eventMethod.Invoke(element, new object[] { args }); } - private void Hooks_OperationPosted(object sender, DispatcherHookEventArgs e) - { - e.Operation.Task.ContinueWith((t) => { - Interlocked.Decrement(ref DispatcherOpsCounter); - }, TaskScheduler.Default); - Interlocked.Increment(ref DispatcherOpsCounter); - } - /// /// Derived test classes can override this method to provide different configurations. /// @@ -130,13 +200,6 @@ protected virtual DynamoModel.IStartConfiguration CreateStartConfiguration(IPath [TearDown] public void Exit() { - - Dispatcher.CurrentDispatcher.Hooks.OperationPosted -= Hooks_OperationPosted; - if (!SkipDispatcherFlush) - { - DispatcherUtil.DoEventsLoop(() => DispatcherOpsCounter == 0); - } - //Ensure that we leave the workspace marked as //not having changes. ViewModel.HomeSpace.HasUnsavedChanges = false; @@ -166,38 +229,7 @@ public void Exit() { Console.WriteLine(ex.StackTrace); } - - TestUtilities.WebView2Tag = string.Empty; - using (var currentProc = Process.GetCurrentProcess()) - { - int id = currentProc.Id; - var name = TestContext.CurrentContext.Test.Name; - System.Console.WriteLine($"PID {id} Finished test: {name} with DispatcherOpsCounter = {DispatcherOpsCounter}"); - System.Console.WriteLine($"PID {id} Finished test: {name} with WorkingSet = {currentProc.WorkingSet64}"); - System.Console.WriteLine($"PID {id} Finished test: {name} with PrivateBytes = {currentProc.PrivateMemorySize64}"); - System.Console.Write($"PID {id} Finished test: {name} with GC Memory Info: "); - PrettyPrint(GC.GetGCMemoryInfo()); - } - } - - private static void PrettyPrint(object obj) - { - Type type = obj.GetType(); - PropertyInfo[] properties = type.GetProperties(); - - Console.WriteLine("{"); - foreach (var property in properties) - { - try - { - Console.WriteLine($" {property.Name}: {property.GetValue(obj)}"); - } - catch (Exception e) - { - Console.WriteLine($" {property.Name}: {e.Message}"); - } - } - Console.WriteLine("}"); + testDiagnostics.SetupAfterCleanupDiagnostics(); } protected virtual void GetLibrariesToPreload(List libraries) diff --git a/test/DynamoCoreWpfTests/RecordedTests.cs b/test/DynamoCoreWpfTests/RecordedTests.cs index a3dbf63b1b8..b328e5ead93 100644 --- a/test/DynamoCoreWpfTests/RecordedTests.cs +++ b/test/DynamoCoreWpfTests/RecordedTests.cs @@ -20,12 +20,14 @@ using Dynamo.Tests; using Dynamo.Utilities; using Dynamo.ViewModels; -using DynamoShapeManager; using NUnit.Framework; using ProtoCore; using PythonNodeModels; using SystemTestServices; -using TestServices; +using System.Windows.Threading; +using DynamoCoreWpfTests.Utility; +using System.Diagnostics; +using System.Threading.Tasks; namespace DynamoCoreWpfTests { @@ -38,6 +40,7 @@ public class RecordedUnitTestBase : DynamoViewModelUnitTest protected System.Random randomizer = null; private IEnumerable customNodesToBeLoaded; private CommandCallback commandCallback; + private TestDiagnostics testDiagnostics = new(); // Geometry preloading related members. protected bool preloadGeometry; @@ -51,16 +54,19 @@ public class RecordedUnitTestBase : DynamoViewModelUnitTest public override void Setup() { - base.Setup(); + testDiagnostics.SetupStartupDiagnostics(); + base.Setup(); // Fixed seed randomizer for predictability. randomizer = new System.Random(123456); } public override void Cleanup() { + testDiagnostics.SetupBeforeCleanupDiagnostics(); commandCallback = null; base.Cleanup(); + testDiagnostics.SetupAfterCleanupDiagnostics(); } #endregion diff --git a/test/DynamoCoreWpfTests/SearchSideEffects.cs b/test/DynamoCoreWpfTests/SearchSideEffects.cs index 2ebc1392ce3..6dec3356e94 100644 --- a/test/DynamoCoreWpfTests/SearchSideEffects.cs +++ b/test/DynamoCoreWpfTests/SearchSideEffects.cs @@ -29,10 +29,10 @@ public void WhenStartingDynamoInputAndOutputNodesAreNolongerMissingFromSearch() Assert.IsAssignableFrom( typeof(HomeWorkspaceModel), ViewModel.Model.CurrentWorkspace ); // search and results are correct - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Input"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Input").Wait(); Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count(x => x.Model.Name == "Input")); - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Output"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Output").Wait(); Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count(x => x.Model.Name == "Output")); } @@ -53,10 +53,10 @@ public void WhenHomeWorkspaceIsFocusedInputAndOutputNodesAreMissingFromSearch() Assert.AreEqual(model.CurrentWorkspace.Name, "Home"); // search and results are correct - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Input"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Input").Wait(); Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count(x => x.Model.Name == "Input")); - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Output"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Output").Wait(); Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count(x => x.Model.Name == "Output")); } @@ -72,11 +72,11 @@ public void WhenCustomNodeWorkspaceIsFocusedInputAndOutputNodesArePresentInSearc Assert.AreEqual(model.CurrentWorkspace.Name, "Sequence2"); // search and results are correct - ViewModel.SearchViewModel.SearchAndUpdateResults("Input"); + ViewModel.SearchViewModel.SearchAndUpdateResultsTask("Input").Wait(); Assert.AreEqual(1, ViewModel.SearchViewModel.FilteredResults.Count(x => x.Model.Name == "Input")); Assert.AreEqual("Input", ViewModel.SearchViewModel.FilteredResults.ElementAt(0).Model.Name); - ViewModel.SearchViewModel.SearchAndUpdateResults("Output"); + ViewModel.SearchViewModel.SearchAndUpdateResultsTask("Output").Wait(); Assert.AreEqual(1, ViewModel.SearchViewModel.FilteredResults.Count(x => x.Model.Name == "Output")); Assert.AreEqual("Output", ViewModel.SearchViewModel.FilteredResults.ElementAt(0).Model.Name); @@ -96,7 +96,7 @@ public void WhenStartingDynamoOperatorNodesNolongerMissingFromSearch() foreach (var node in nodesList) { // search and check that the results are correct based in the node name provided for the searchTerm - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults(node); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask(node).Wait(); var filteredResults = ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults; Assert.AreEqual(1, filteredResults.Count(x => x.Model.Name == node), "Non matching results for " + node); } @@ -113,7 +113,7 @@ public void LuceneSearchAllNodesValidation() foreach (var node in nodesList) { // Search and check that the results are correct based in the node name provided for the searchTerm - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults(node); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask(node).Wait(); var filteredResults = ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults; //There are overloaded nodes diff --git a/test/DynamoCoreWpfTests/SearchViewModelTests.cs b/test/DynamoCoreWpfTests/SearchViewModelTests.cs index 813d8251631..8138cd315a3 100644 --- a/test/DynamoCoreWpfTests/SearchViewModelTests.cs +++ b/test/DynamoCoreWpfTests/SearchViewModelTests.cs @@ -39,13 +39,13 @@ public void PopulateSearchTextWithSelectedResultReturnsExpectedResult() model.Add(new CustomNodeSearchElement(null, new CustomNodeInfo(Guid.NewGuid(), "frog", catName, descr, path))); model.Add(new CustomNodeSearchElement(null, new CustomNodeInfo(Guid.NewGuid(), "Noodle", catName, descr, path))); - viewModel.SearchAndUpdateResults("xy"); + viewModel.SearchAndUpdateResultsTask("xy").Wait(); Assert.AreEqual("xyz", viewModel.SearchText); - viewModel.SearchAndUpdateResults("ood"); + viewModel.SearchAndUpdateResultsTask("ood").Wait(); Assert.AreEqual("Noodle", viewModel.SearchText); - viewModel.SearchAndUpdateResults("do"); + viewModel.SearchAndUpdateResultsTask("do").Wait(); Assert.AreEqual("dog", viewModel.SearchText); } @@ -660,7 +660,7 @@ public void FirstItemIsSelectedAfterSearch() model.Add(element); viewModel.Visible = true; - viewModel.SearchAndUpdateResults("member"); + viewModel.SearchAndUpdateResultsTask("member").Wait(); Assert.AreEqual(2, viewModel.FilteredResults.Count()); Assert.IsTrue(viewModel.FilteredResults.ElementAt(0).IsSelected); @@ -671,7 +671,7 @@ public void FirstItemIsSelectedAfterSearch() public void NoItemsIsSelectedAfterSearch() { viewModel.Visible = true; - viewModel.SearchAndUpdateResults("member"); + viewModel.SearchAndUpdateResultsTask("member").Wait(); Assert.DoesNotThrow(() => viewModel.MoveSelection(SearchViewModel.Direction.Down)); Assert.IsFalse(viewModel.FilteredResults.Any()); @@ -688,7 +688,7 @@ public void MoveForward() model.Add(element); viewModel.Visible = true; - viewModel.SearchAndUpdateResults("member"); + viewModel.SearchAndUpdateResultsTask("member").Wait(); Assert.AreEqual(2, viewModel.FilteredResults.Count()); Assert.IsTrue(viewModel.FilteredResults.ElementAt(0).IsSelected); @@ -711,7 +711,7 @@ public void MoveBack() model.Add(element); viewModel.Visible = true; - viewModel.SearchAndUpdateResults("member"); + viewModel.SearchAndUpdateResultsTask("member").Wait(); Assert.AreEqual(2, viewModel.FilteredResults.Count()); Assert.IsTrue(viewModel.FilteredResults.ElementAt(0).IsSelected); @@ -741,7 +741,7 @@ public void SelectAllSearchCategories() model.Add(element); viewModel.Visible = true; - viewModel.SearchAndUpdateResults("member"); + viewModel.SearchAndUpdateResultsTask("member").Wait(); Assert.AreEqual(2, viewModel.FilteredResults.Count()); Assert.IsTrue(viewModel.SearchCategories.All(c => c.IsSelected)); diff --git a/test/DynamoCoreWpfTests/SerializationTests.cs b/test/DynamoCoreWpfTests/SerializationTests.cs index befd4c7f2df..dc2ad4fca39 100644 --- a/test/DynamoCoreWpfTests/SerializationTests.cs +++ b/test/DynamoCoreWpfTests/SerializationTests.cs @@ -548,10 +548,12 @@ protected override void GetLibrariesToPreload(List libraries) class JSONSerializationTests : DynamoViewModelUnitTest { - public static string jsonNonGuidFolderName = "jsonWithView_nonGuidIds"; - public static string jsonFolderName = "jsonWithView"; + private static Dictionary testFilesCache = []; - private const string jsonStructuredFolderName = "DynamoCoreWPFTests"; + public static string jsonNonGuidFolderName = $"jsonWithView_nonGuidIds_{Environment.ProcessId}"; + public static string jsonFolderName = $"jsonWithView_{Environment.ProcessId}"; + + private static string jsonStructuredFolderName = $"DynamoCoreWPFTests_{Environment.ProcessId}"; private TimeSpan lastExecutionDuration = new TimeSpan(); private Dictionary modelsGuidToIdMap = new Dictionary(); @@ -943,6 +945,7 @@ public void FixtureSetup() Console.WriteLine(e.Message); } } + CacheTestFiles(); } [OneTimeTearDown] @@ -961,11 +964,14 @@ private void ExecutionEvents_GraphPostExecution(Dynamo.Session.IExecutionSession /// the test directory, opens them and executes, then converts them to /// json and executes again, comparing the values from the two runs. /// - /// The path to a .dyn file. This parameter is supplied - /// by the test framework. + /// A random guid assigned to a .dyn file. This parameter is supplied + /// by the test framework. You can get the file path by calling GetTestFileNameFromGuid(fileId). [Test, TestCaseSource(nameof(FindWorkspaces)), Category("JsonTestExclude")] - public void SerializationTest(string filePath) + public void SerializationTest(string fileId) { + string filePath = GetTestFileNameFromGuid(fileId); + Console.WriteLine($"Running test {TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.MethodName} with file {filePath}"); + DoWorkspaceOpenAndCompareView(filePath, jsonFolderName, ConvertCurrentWorkspaceViewToJsonAndSave, @@ -980,11 +986,14 @@ public void SerializationTest(string filePath) /// This set of tests has slightly modified json where the id properties /// are altered when serialized to test deserialization of non-guid ids. /// - /// The path to a .dyn file. This parameter is supplied - /// by the test framework. + /// A random guid assigned to a .dyn file. This parameter is supplied + /// by the test framework. You can get the file path by calling GetTestFileNameFromGuid(fileId). [Test, TestCaseSource(nameof(FindWorkspaces)), Category("JsonTestExclude")] - public void SerializationNonGuidIdsTest(string filePath) + public void SerializationNonGuidIdsTest(string fileId) { + string filePath = GetTestFileNameFromGuid(fileId); + Console.WriteLine($"Running test {TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.MethodName} with file {filePath}"); + modelsGuidToIdMap.Clear(); DoWorkspaceOpenAndCompareView(filePath, jsonNonGuidFolderName, @@ -1142,12 +1151,28 @@ public void SelectionNodeInputDataSerializationTest() serializationTestUtils.SaveWorkspaceComparisonData); } - public static object[] FindWorkspaces() + private void CacheTestFiles() { + testFilesCache.Clear(); var di = new DirectoryInfo(TestDirectory); var fis = new string[] { "*.dyn", "*.dyf" } .SelectMany(i => di.GetFiles(i, SearchOption.AllDirectories)); - return fis.Select(fi => fi.FullName).Take(MAXNUM_SERIALIZATIONTESTS_TOEXECUTE).ToArray(); + var testFiles = fis.Select(fi => fi.FullName).Take(MAXNUM_SERIALIZATIONTESTS_TOEXECUTE).ToArray(); + + foreach (var testFile in testFiles) + { + testFilesCache.Add(Guid.NewGuid().ToString(), testFile); + } + } + + internal static string GetTestFileNameFromGuid(string guid) + { + return testFilesCache.GetValueOrDefault(guid); + } + + public static object[] FindWorkspaces() + { + return [.. testFilesCache.Keys]; } diff --git a/test/DynamoCoreWpfTests/SplashScreenTests.cs b/test/DynamoCoreWpfTests/SplashScreenTests.cs deleted file mode 100644 index 7c28cb5a5c2..00000000000 --- a/test/DynamoCoreWpfTests/SplashScreenTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using DynamoUtilities; -using DynamoCoreWpfTests.Utility; -using NUnit.Framework; - -namespace DynamoCoreWpfTests -{ - - [TestFixture] - internal class SplashScreenTests - { - public enum WindowsMessage - { - WM_CLOSE = 0x0010 - } - - [DllImport("user32.dll")] - public static extern IntPtr FindWindow(string lpClassName, String lpWindowName); - - [DllImport("user32.dll")] - public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - static extern uint RegisterWindowMessage(string lpString); - - - [SetUp] - public void SetUp() - { - TestUtilities.WebView2Tag = TestContext.CurrentContext.Test.Name; - } - - [TearDown] - public void CleanUp() - { - TestUtilities.WebView2Tag = string.Empty; - } - - [Test] - //note that this test sends a windows close message directly to the window - //but skips the JS interop that users rely on to close the window - so that is not tested by this test. - public void SplashScreen_MultipleCloseMessages() - { - var ss = new Dynamo.UI.Views.SplashScreen(); - ss.Title = "Dynamo SplashScreen Test"; - - void WebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e) - { - ss.webView.NavigationCompleted -= WebView_NavigationCompleted; - - IntPtr WindowToFind = FindWindow(null, "Dynamo SplashScreen Test"); - Debug.Assert(WindowToFind != IntPtr.Zero); - - // Simulate clicking on the close button several times while the main thread is stuck waiting. - _ = Task.Run(() => - { - Thread.Sleep(100); - _ = SendMessage(WindowToFind, (int)WindowsMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); - }); - _ = Task.Run(() => - { - Thread.Sleep(100); - _ = SendMessage(WindowToFind, (int)WindowsMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); - }); - - Task.Delay(1000).Wait(); - } - ss.webView.NavigationCompleted += WebView_NavigationCompleted; - - bool windowClosed = false; - void WindowClosed(object sender, EventArgs e) - { - windowClosed = true; - } - - ss.Closed += WindowClosed; - - ss.Show(); - - DispatcherUtil.DoEventsLoop(() => windowClosed, 50); - - ss.Closed -= WindowClosed; - - Assert.IsTrue(windowClosed);// Make sure the window was closed - } - } -} diff --git a/test/DynamoCoreWpfTests/WorkspaceSaving.cs b/test/DynamoCoreWpfTests/WorkspaceSaving.cs index 53dde8b92b3..c42f77d0a47 100644 --- a/test/DynamoCoreWpfTests/WorkspaceSaving.cs +++ b/test/DynamoCoreWpfTests/WorkspaceSaving.cs @@ -966,7 +966,7 @@ public void CustomNodeEditNodeDescriptionKeepingViewBlockInDyf() workspace.FileName = GetNewFileNameOnTempPath("dyf"); // search common base name ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.Visible = true; - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Cool"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Cool").Wait(); // results are correct Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count()); @@ -1123,7 +1123,7 @@ public void CustomNodeSaveAsAddsNewCustomNodeToSearch() var newId = nodeWorkspace.CustomNodeDefinition.FunctionId; ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.Visible = true; - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Constant2"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Constant2").Wait(); Assert.AreEqual(originalNumElements + 1, ViewModel.Model.SearchModel.NumElements); Assert.AreEqual(2, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.OfType().Count()); @@ -1226,7 +1226,7 @@ public void CustomNodeSaveAsAddsNewCustomNodeToSearchAndItCanBeRefactoredWhilePr // search for refactored node ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.Visible = true; - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("TheNoodle"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("TheNoodle").Wait(); // results are correct Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count()); @@ -1234,7 +1234,7 @@ public void CustomNodeSaveAsAddsNewCustomNodeToSearchAndItCanBeRefactoredWhilePr Assert.AreEqual(newId, node3.ID); // search for un-refactored node - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Constant2"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Constant2").Wait(); // results are correct Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count()); @@ -1282,7 +1282,7 @@ public void CustomNodeSaveAsAddsNewCustomNodeToSearchAndItCanBeRefactoredWhilePr // search common base name ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.Visible = true; - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults("Constant2"); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask("Constant2").Wait(); // results are correct Assert.AreEqual(2, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.OfType().Count()); @@ -1374,7 +1374,7 @@ public void CusotmNodeSaveAsUpdateItsName() Assert.AreEqual(newName, workspace.Name); // Verify new name is searchable - ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResults(newName); + ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.SearchAndUpdateResultsTask(newName).Wait(); Assert.AreEqual(1, ViewModel.CurrentSpaceViewModel.InCanvasSearchViewModel.FilteredResults.Count()); // Verify search element's name is new name