mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
DecompileAsEnumerableTest exercises the full path: open mscorlib, navigate to Enumerable.AsEnumerable, render the decompilation, verify text and verify the row centred in the assembly tree. Writing it surfaced two pre-existing bugs the manual smoke-test missed:pull/3755/head
8 changed files with 557 additions and 11 deletions
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using global::ILSpy.AppEnv; |
||||
using global::ILSpy.TreeNodes; |
||||
using global::ILSpy.ViewModels; |
||||
using global::ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
[TestFixture] |
||||
public class DecompileAsEnumerableTest |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Selecting_AsEnumerable_Method_Decompiles_Into_Document_View() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
|
||||
// MainWindow.Opened triggers AssemblyTreeModel.Initialize, which queues the default
|
||||
// assembly list (System.Private.CoreLib, System, System.Linq) for async loading.
|
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); |
||||
|
||||
// Walk via the production FindNodeByPath (used by SessionSettings.ActiveTreeViewPath
|
||||
// save/restore) down to the type. First segment is the assembly short name; the helper
|
||||
// substitutes the file path AssemblyTreeNode.ToString returns.
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>( |
||||
"System.Linq", "System.Linq", "System.Linq.Enumerable"); |
||||
|
||||
// Method-level stable identities are built via ILAmbience and impractical to author by
|
||||
// hand, so we expand the type and pick the overload by name. Enumerable has only one
|
||||
// AsEnumerable, so this is unambiguous.
|
||||
typeNode.EnsureLazyChildren(); |
||||
var methodNode = typeNode.Children.OfType<MethodTreeNode>() |
||||
.Single(m => m.MethodDefinition.Name == "AsEnumerable"); |
||||
methodNode.MethodDefinition.IsExtensionMethod.Should().BeTrue(); |
||||
|
||||
// Selecting the node routes through DockWorkspace -> the active decompiler tab.
|
||||
vm.AssemblyTreeModel.SelectedItem = methodNode; |
||||
|
||||
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); |
||||
tab.Text.Should().Contain("AsEnumerable"); |
||||
tab.Text.Should().Contain("IEnumerable<TSource>"); |
||||
tab.Text.Should().Contain("return source"); |
||||
|
||||
// Selecting a deeply-nested node must scroll the assembly tree so it's centered.
|
||||
methodNode.Should().Be().CenteredInView(); |
||||
|
||||
window.CaptureAndShow(); |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using global::ILSpy.AppEnv; |
||||
using global::ILSpy.ViewModels; |
||||
using global::ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
[TestFixture] |
||||
public class MainWindowTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public void MainWindow_Resolves_From_Composition_And_Shows() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
|
||||
window.IsVisible.Should().BeTrue(); |
||||
window.Title.Should().Be("ILSpy"); |
||||
window.DataContext.Should().BeOfType<MainWindowViewModel>(); |
||||
|
||||
window.CaptureAndShow(); |
||||
} |
||||
} |
||||
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using global::ILSpy.AssemblyTree; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
/// <summary>
|
||||
/// Path-based tree navigation built on the production
|
||||
/// <see cref="AssemblyTreeModel.FindNodeByPath(string[], bool)"/> walk. First segment is an
|
||||
/// assembly short name (looked up in the assembly list, substituted with its file path);
|
||||
/// the rest are the stable <c>ToString()</c> identities each tree node emits — namespace
|
||||
/// name, type reflection name. Member-level identities go through ILAmbience and aren't
|
||||
/// hand-authorable, so tests should drill to the type with <see cref="FindNode{T}"/> and
|
||||
/// pick the member from <c>typeNode.Children</c> by <c>Name</c>.
|
||||
/// </summary>
|
||||
public static class TreeNavigation |
||||
{ |
||||
public static SharpTreeNode FindNode(this AssemblyTreeModel atm, params string[] segments) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(atm); |
||||
ArgumentNullException.ThrowIfNull(segments); |
||||
if (segments.Length == 0) |
||||
throw new ArgumentException("At least one segment (the assembly short name) is required.", nameof(segments)); |
||||
|
||||
var path = ResolveAssembly(atm, segments); |
||||
return atm.FindNodeByPath(path, returnBestMatch: false) |
||||
?? Diagnose(atm, path, segments); |
||||
} |
||||
|
||||
public static T FindNode<T>(this AssemblyTreeModel atm, params string[] segments) |
||||
where T : SharpTreeNode |
||||
{ |
||||
var node = FindNode(atm, segments); |
||||
return node as T |
||||
?? throw new InvalidOperationException( |
||||
$"Path [{string.Join(" / ", segments)}] resolved to {node.GetType().Name}, expected {typeof(T).Name}."); |
||||
} |
||||
|
||||
public static SharpTreeNode SelectNode(this AssemblyTreeModel atm, params string[] segments) |
||||
{ |
||||
var node = FindNode(atm, segments); |
||||
atm.SelectedItem = node; |
||||
return node; |
||||
} |
||||
|
||||
public static T SelectNode<T>(this AssemblyTreeModel atm, params string[] segments) |
||||
where T : SharpTreeNode |
||||
{ |
||||
var node = FindNode<T>(atm, segments); |
||||
atm.SelectedItem = node; |
||||
return node; |
||||
} |
||||
|
||||
static string[] ResolveAssembly(AssemblyTreeModel atm, string[] segments) |
||||
{ |
||||
var shortName = segments[0]; |
||||
var asm = atm.AssemblyList?.GetAssemblies() |
||||
.FirstOrDefault(a => string.Equals(a.ShortName, shortName, StringComparison.Ordinal)) |
||||
?? throw new InvalidOperationException( |
||||
$"Assembly with ShortName '{shortName}' is not in the active list. Loaded: " + |
||||
$"[{string.Join(", ", atm.AssemblyList?.GetAssemblies().Select(a => a.ShortName) ?? Array.Empty<string>())}]"); |
||||
|
||||
var translated = (string[])segments.Clone(); |
||||
translated[0] = asm.FileName; |
||||
return translated; |
||||
} |
||||
|
||||
static SharpTreeNode Diagnose(AssemblyTreeModel atm, string[] stablePath, string[] originalSegments) |
||||
{ |
||||
var partial = atm.FindNodeByPath(stablePath, returnBestMatch: true); |
||||
var children = partial == null |
||||
? "(root null)" |
||||
: string.Join(", ", partial.Children.Select(c => c.ToString())); |
||||
throw new InvalidOperationException( |
||||
$"FindNodeByPath could not resolve [{string.Join(" / ", originalSegments)}]. " + |
||||
$"Reached '{partial?.ToString() ?? "(null)"}'; available children: [{children}]."); |
||||
} |
||||
} |
||||
@ -0,0 +1,202 @@
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
|
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Threading; |
||||
using Avalonia.VisualTree; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using global::ILSpy.AppEnv; |
||||
using global::ILSpy.AssemblyTree; |
||||
using global::ILSpy.Views; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
public static class TreeNodeAssertionsExtensions |
||||
{ |
||||
public static TreeNodeAssertions Should(this SharpTreeNode subject) => new(subject); |
||||
} |
||||
|
||||
public class TreeNodeAssertions |
||||
{ |
||||
public SharpTreeNode Subject { get; } |
||||
|
||||
public TreeNodeAssertions(SharpTreeNode subject) |
||||
{ |
||||
Subject = subject ?? throw new ArgumentNullException(nameof(subject)); |
||||
} |
||||
|
||||
public TreeNodeBeAssertions Be() => new(Subject); |
||||
} |
||||
|
||||
public class TreeNodeBeAssertions |
||||
{ |
||||
static readonly TimeSpan ScrollPollTimeout = TimeSpan.FromSeconds(3); |
||||
|
||||
public SharpTreeNode Subject { get; } |
||||
|
||||
public TreeNodeBeAssertions(SharpTreeNode subject) |
||||
{ |
||||
Subject = subject; |
||||
} |
||||
|
||||
public AndConstraint<TreeNodeBeAssertions> ScrolledIntoView(string because = "", params object[] becauseArgs) |
||||
=> AssertScrollState(centered: false, because, becauseArgs); |
||||
|
||||
public AndConstraint<TreeNodeBeAssertions> CenteredInView(string because = "", params object[] becauseArgs) |
||||
=> AssertScrollState(centered: true, because, becauseArgs); |
||||
|
||||
AndConstraint<TreeNodeBeAssertions> AssertScrollState(bool centered, string because, object[] becauseArgs) |
||||
{ |
||||
var grid = TryFindGrid(out var failure); |
||||
if (grid is null) |
||||
{ |
||||
false.Should().BeTrue( |
||||
$"cannot verify scroll state for {Describe()}: {failure}{(string.IsNullOrEmpty(because) ? "" : " " + string.Format(because, becauseArgs))}"); |
||||
return new AndConstraint<TreeNodeBeAssertions>(this); |
||||
} |
||||
|
||||
var scrollViewer = grid.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault() |
||||
?? throw new InvalidOperationException("DataGrid has no ScrollViewer in its visual tree."); |
||||
|
||||
// ScrollIntoView is posted at Background priority — give it time to realise the row.
|
||||
var deadline = DateTime.UtcNow + ScrollPollTimeout; |
||||
DataGridRow? row = null; |
||||
bool ok = false; |
||||
while (DateTime.UtcNow < deadline) |
||||
{ |
||||
Dispatcher.UIThread.RunJobs(); |
||||
row = FindRow(grid, Subject); |
||||
if (row != null && IsInViewport(row, scrollViewer) |
||||
&& (!centered || IsRoughlyCentered(row, scrollViewer))) |
||||
{ |
||||
ok = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
ok.Should().BeTrue( |
||||
$"{Describe()} should be {(centered ? "centred" : "scrolled into view")}{(string.IsNullOrEmpty(because) ? "" : ", " + string.Format(because, becauseArgs))}, " + |
||||
BuildState(scrollViewer, row)); |
||||
|
||||
return new AndConstraint<TreeNodeBeAssertions>(this); |
||||
} |
||||
|
||||
static DataGrid? TryFindGrid(out string? failure) |
||||
{ |
||||
var window = FindActiveWindow(); |
||||
if (window is null) |
||||
{ |
||||
failure = "no active Avalonia Window"; |
||||
return null; |
||||
} |
||||
|
||||
var pane = window.GetVisualDescendants().OfType<AssemblyListPane>().FirstOrDefault(); |
||||
if (pane is null) |
||||
{ |
||||
failure = "AssemblyListPane is not in the visual tree (the assembly tree pane isn't shown)"; |
||||
return null; |
||||
} |
||||
|
||||
var grid = pane.GetVisualDescendants().OfType<DataGrid>().FirstOrDefault(g => g.Name == "TreeGrid"); |
||||
if (grid is null) |
||||
{ |
||||
failure = "TreeGrid DataGrid not found inside AssemblyListPane"; |
||||
return null; |
||||
} |
||||
|
||||
failure = null; |
||||
return grid; |
||||
} |
||||
|
||||
static Window? FindActiveWindow() |
||||
{ |
||||
// ApplicationLifetime is null in headless; the [Shared] MainWindow export gives us
|
||||
// the same instance the test resolved.
|
||||
try |
||||
{ |
||||
return AppComposition.Current.GetExport<MainWindow>(); |
||||
} |
||||
catch |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
static DataGridRow? FindRow(DataGrid grid, SharpTreeNode target) |
||||
{ |
||||
foreach (var row in grid.GetVisualDescendants().OfType<DataGridRow>()) |
||||
{ |
||||
var ctx = row.DataContext; |
||||
if (ReferenceEquals(ctx, target)) |
||||
return row; |
||||
// HierarchicalNode wraps the underlying item via .Item.
|
||||
var itemProp = ctx?.GetType().GetProperty("Item"); |
||||
if (itemProp is not null && ReferenceEquals(itemProp.GetValue(ctx), target)) |
||||
return row; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
static bool IsInViewport(DataGridRow row, ScrollViewer scrollViewer) |
||||
{ |
||||
var topLeft = row.TranslatePoint(new Point(0, 0), scrollViewer); |
||||
if (topLeft is null) |
||||
return false; |
||||
var top = topLeft.Value.Y; |
||||
var bottom = top + row.Bounds.Height; |
||||
return top >= 0 && bottom <= scrollViewer.Viewport.Height + 0.5; |
||||
} |
||||
|
||||
static bool IsRoughlyCentered(DataGridRow row, ScrollViewer scrollViewer) |
||||
{ |
||||
var topLeft = row.TranslatePoint(new Point(0, 0), scrollViewer); |
||||
if (topLeft is null) |
||||
return false; |
||||
var rowMid = topLeft.Value.Y + row.Bounds.Height / 2; |
||||
var viewportMid = scrollViewer.Viewport.Height / 2; |
||||
// Accept rows clamped at the start/end of the list as "as centred as possible".
|
||||
var nearTopClamp = scrollViewer.Offset.Y <= 0.5; |
||||
var nearBottomClamp = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height - 0.5; |
||||
return Math.Abs(rowMid - viewportMid) <= row.Bounds.Height || nearTopClamp || nearBottomClamp; |
||||
} |
||||
|
||||
string BuildState(ScrollViewer scrollViewer, DataGridRow? row) |
||||
{ |
||||
if (row is null) |
||||
return $"but no DataGridRow has been realised for it (offset={scrollViewer.Offset}, viewport={scrollViewer.Viewport}, extent={scrollViewer.Extent})"; |
||||
var topLeft = row.TranslatePoint(new Point(0, 0), scrollViewer); |
||||
var top = topLeft?.Y; |
||||
var bottom = top + row.Bounds.Height; |
||||
return $"but its row sits at {top:0.#}..{bottom:0.#} relative to the viewport " + |
||||
$"(viewport height {scrollViewer.Viewport.Height:0.#}, offset {scrollViewer.Offset.Y:0.#}, extent {scrollViewer.Extent.Height:0.#})"; |
||||
} |
||||
|
||||
string Describe() |
||||
{ |
||||
var text = Subject.Text?.ToString(); |
||||
return string.IsNullOrEmpty(text) ? Subject.GetType().Name : $"\"{text}\""; |
||||
} |
||||
} |
||||
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Threading; |
||||
|
||||
using global::ILSpy.AssemblyTree; |
||||
using global::ILSpy.Docking; |
||||
using global::ILSpy.TextView; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
public static class Waiters |
||||
{ |
||||
static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); |
||||
static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(25); |
||||
|
||||
public static async Task WaitForAsync( |
||||
Func<bool> predicate, |
||||
TimeSpan? timeout = null, |
||||
[CallerArgumentExpression(nameof(predicate))] string? description = null) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(predicate); |
||||
var deadline = DateTime.UtcNow + (timeout ?? DefaultTimeout); |
||||
while (DateTime.UtcNow < deadline) |
||||
{ |
||||
if (predicate()) |
||||
return; |
||||
Dispatcher.UIThread.RunJobs(); |
||||
await Task.Delay(PollInterval); |
||||
} |
||||
throw new TimeoutException( |
||||
$"Timed out after {(timeout ?? DefaultTimeout).TotalSeconds:0.#}s waiting for: {description}"); |
||||
} |
||||
|
||||
public static async Task WaitForAssembliesAsync( |
||||
this AssemblyTreeModel atm, |
||||
int minimumCount = 1, |
||||
TimeSpan? timeout = null) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(atm); |
||||
await WaitForAsync( |
||||
() => (atm.AssemblyList?.GetAssemblies().Length ?? 0) >= minimumCount, |
||||
timeout, |
||||
$"AssemblyList to contain >= {minimumCount} assemblies"); |
||||
var assemblies = atm.AssemblyList!.GetAssemblies(); |
||||
await Task.WhenAll(assemblies.Select(a => a.GetLoadResultAsync())); |
||||
} |
||||
|
||||
public static async Task<DecompilerTabPageModel> WaitForDecompiledTextAsync( |
||||
this DockWorkspace dock, |
||||
TimeSpan? timeout = null) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(dock); |
||||
var documents = ((ILSpyDockFactory)dock.Factory).Documents |
||||
?? throw new InvalidOperationException("DockWorkspace has no document dock yet."); |
||||
|
||||
await WaitForAsync( |
||||
() => documents.ActiveDockable is DecompilerTabPageModel { IsDecompiling: false } tab |
||||
&& !string.IsNullOrEmpty(tab.Text), |
||||
timeout, |
||||
"active decompiler tab to finish decompiling and produce text"); |
||||
|
||||
return (DecompilerTabPageModel)documents.ActiveDockable!; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue