mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
12 KiB
319 lines
12 KiB
// Copyright (c) 2026 Christoph Wille |
|
// |
|
// 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.IO; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
|
|
using Avalonia.Headless.NUnit; |
|
|
|
using AwesomeAssertions; |
|
|
|
using ICSharpCode.ILSpy.Processes; |
|
using ICSharpCode.ILSpy.ViewModels; |
|
|
|
using NUnit.Framework; |
|
|
|
namespace ICSharpCode.ILSpy.Tests.Processes; |
|
|
|
/// <summary> |
|
/// Behavior of the "Open from Running Process" dialog's view model against a fake explorer: |
|
/// which processes are listed and filtered, when a process's assemblies are fetched, and |
|
/// what the dialog hands back to be opened. Assemblies that exist only in a process's |
|
/// memory are listed but cannot be opened, which several of these tests pin down. |
|
/// </summary> |
|
[TestFixture] |
|
public class OpenFromProcessDialogViewModelTests |
|
{ |
|
static string TestFile(string name) => Path.Combine(TestContext.CurrentContext.TestDirectory, name); |
|
|
|
static ProcessModuleInfo OnDisk(string name) => new(name, TestFile(name), IsInMemory: false); |
|
|
|
static ProcessModuleInfo InMemory(string name) => new(name, Path: null, IsInMemory: true); |
|
|
|
static (OpenFromProcessDialogViewModel vm, FakeProcessExplorer explorer) CreateViewModel() |
|
{ |
|
var explorer = new FakeProcessExplorer(); |
|
explorer.ProcessesToReturn.Add(FakeProcessExplorer.Process(100, "ILSpy", "ILSpy")); |
|
explorer.ProcessesToReturn.Add(FakeProcessExplorer.Process(200, "dotnet", "MyTool")); |
|
explorer.ModulesByPid[100] = new[] { OnDisk("ILSpy.dll"), OnDisk("ICSharpCode.Decompiler.dll") }; |
|
explorer.ModulesByPid[200] = new[] { OnDisk("ILSpy.Tests.dll"), InMemory("Dynamic.Proxies") }; |
|
return (new OpenFromProcessDialogViewModel(explorer), explorer); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Refresh_Lists_The_Running_Processes() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
|
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
|
|
vm.Processes.Select(p => p.Pid).Should().Equal(new[] { 100, 200 }); |
|
vm.Processes[0].ProcessName.Should().Be("ILSpy"); |
|
vm.IsLoadingProcesses.Should().BeFalse(); |
|
explorer.ProcessCalls.Should().Be(1); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task The_Filter_Matches_Process_Name_Pid_And_Entry_Assembly() |
|
{ |
|
var (vm, _) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
|
|
vm.FilterText = "ilspy"; |
|
vm.Processes.Select(p => p.Pid).Should().Equal(new[] { 100 }, "the name matches case-insensitively"); |
|
|
|
vm.FilterText = "200"; |
|
vm.Processes.Select(p => p.Pid).Should().Equal(new[] { 200 }, "a pid is a natural thing to search for"); |
|
|
|
vm.FilterText = "MyTool"; |
|
vm.Processes.Select(p => p.Pid).Should().Equal(new[] { 200 }, "the entry assembly is the interesting name"); |
|
|
|
vm.FilterText = ""; |
|
vm.Processes.Should().HaveCount(2); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Selecting_A_Process_Loads_Its_Assemblies() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
|
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 2); |
|
|
|
explorer.ModuleCalls.Should().Equal(new[] { 100 }); |
|
vm.Modules.Select(m => m.Name).Should().Equal(new[] { "ILSpy.dll", "ICSharpCode.Decompiler.dll" }); |
|
vm.IsLoadingModules.Should().BeFalse(); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Clearing_The_Selection_Empties_The_Assembly_List() |
|
{ |
|
var (vm, _) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 2); |
|
|
|
vm.SelectedProcess = null; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 0); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Add_Selected_Closes_The_Dialog_With_The_Assembly_Paths() |
|
{ |
|
var (vm, _) = CreateViewModel(); |
|
string[]? closedWith = null; |
|
vm.CloseRequested += paths => closedWith = paths; |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 2); |
|
|
|
vm.SelectedModules.Add(vm.Modules[0]); |
|
vm.SelectedModules.Add(vm.Modules[1]); |
|
vm.AddSelectedModulesCommand.Execute(null); |
|
|
|
closedWith.Should().Equal(new[] { TestFile("ILSpy.dll"), TestFile("ICSharpCode.Decompiler.dll") }); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Assemblies_That_Exist_Only_In_Memory_Cannot_Be_Added() |
|
{ |
|
var (vm, _) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
vm.SelectedProcess = vm.Processes[1]; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 2); |
|
|
|
var dynamicModule = vm.Modules.Single(m => m.IsInMemory); |
|
vm.SelectedModules.Add(dynamicModule); |
|
|
|
vm.AddSelectedModulesCommand.CanExecute(null).Should().BeFalse( |
|
"an assembly with no file on disk cannot be opened"); |
|
|
|
vm.SelectedModules.Add(vm.Modules.Single(m => !m.IsInMemory)); |
|
vm.AddSelectedModulesCommand.CanExecute(null).Should().BeTrue(); |
|
|
|
string[]? closedWith = null; |
|
vm.CloseRequested += paths => closedWith = paths; |
|
vm.AddSelectedModulesCommand.Execute(null); |
|
|
|
closedWith.Should().Equal(new[] { TestFile("ILSpy.Tests.dll") }, |
|
"the in-memory row is skipped rather than blocking the rest"); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Add_Entry_Assembly_Closes_With_The_Assembly_Behind_The_Host() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
explorer.ProcessesToReturn.Clear(); |
|
explorer.ProcessesToReturn.Add(FakeProcessExplorer.Process(300, "MyApp", "ILSpy.Tests")); |
|
explorer.ModulesByPid[300] = new[] { OnDisk("ILSpy.Tests.dll"), OnDisk("ILSpy.dll") }; |
|
string[]? closedWith = null; |
|
vm.CloseRequested += paths => closedWith = paths; |
|
|
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 1); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.AddEntryAssemblyCommand.CanExecute(null)); |
|
|
|
vm.AddEntryAssemblyCommand.Execute(null); |
|
|
|
closedWith.Should().Equal(new[] { TestFile("ILSpy.Tests.dll") }); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task A_Process_Whose_Entry_Assembly_Is_Unknown_Offers_Nothing_To_Add() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
explorer.ProcessesToReturn.Clear(); |
|
explorer.ProcessesToReturn.Add(FakeProcessExplorer.Process(400, "ghost", entryAssembly: null)); |
|
explorer.ModulesByPid[400] = Array.Empty<ProcessModuleInfo>(); |
|
|
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 1); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => !vm.IsLoadingModules); |
|
|
|
vm.AddEntryAssemblyCommand.CanExecute(null).Should().BeFalse(); |
|
vm.AddSelectedModulesCommand.CanExecute(null).Should().BeFalse(); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task A_Superseded_Assembly_Load_Does_Not_Overwrite_The_Current_One() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
|
|
var gate = new TaskCompletionSource(); |
|
explorer.ModulesGate = gate; |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => explorer.ModuleCalls.Count == 1); |
|
|
|
// The user moves on before the first process answers. |
|
explorer.ModulesGate = null; |
|
vm.SelectedProcess = vm.Processes[1]; |
|
await Waiters.WaitForAsync(() => vm.Modules.Count == 2); |
|
gate.SetResult(); |
|
await Task.Delay(50); |
|
|
|
vm.Modules.Select(m => m.Name).Should().Equal(new[] { "ILSpy.Tests.dll", "Dynamic.Proxies" }, |
|
"the abandoned query must not paint its result over the current selection"); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task A_Failing_Process_Scan_Surfaces_The_Error_And_Clears_On_Retry() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
explorer.ProcessesException = new IOException("the diagnostics port is unreachable"); |
|
|
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.ErrorMessage != null); |
|
|
|
vm.ErrorMessage.Should().Contain("the diagnostics port is unreachable"); |
|
vm.IsLoadingProcesses.Should().BeFalse(); |
|
|
|
explorer.ProcessesException = null; |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
vm.ErrorMessage.Should().BeNull("a successful retry must dismiss the stale error"); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task A_Failing_Assembly_Query_Surfaces_The_Error_Without_Closing() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
bool closed = false; |
|
vm.CloseRequested += _ => closed = true; |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
|
|
explorer.ModulesException = new UnauthorizedAccessException("access denied"); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.ErrorMessage != null); |
|
|
|
vm.ErrorMessage.Should().Contain("access denied"); |
|
vm.Modules.Should().BeEmpty(); |
|
vm.IsLoadingModules.Should().BeFalse(); |
|
closed.Should().BeFalse(); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Refreshing_While_Assemblies_Are_Loading_Stops_The_Progress_Bar() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
explorer.ModulesGate = new TaskCompletionSource(); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.IsLoadingModules); |
|
|
|
// Refresh drops the selection, so no assembly list is being loaded any more - while |
|
// the query that was in flight has been superseded and can no longer report that it |
|
// stopped. Something has to turn the indicator off, or it animates over an empty pane |
|
// until the user selects another process. |
|
explorer.ModulesGate = null; |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.SelectedProcess == null && vm.Processes.Count == 2); |
|
|
|
vm.IsLoadingModules.Should().BeFalse("nothing is loading, so nothing may animate"); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task A_Failed_Scan_Leaves_Nothing_Selected_To_Act_On() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => vm.AddEntryAssemblyCommand.CanExecute(null)); |
|
|
|
explorer.ProcessesException = new IOException("the diagnostics port is unreachable"); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.ErrorMessage != null); |
|
|
|
// The list the selection pointed into has just been emptied; leaving the selection |
|
// behind keeps the entry-assembly button armed against a row nobody can see. |
|
vm.Processes.Should().BeEmpty(); |
|
vm.SelectedProcess.Should().BeNull(); |
|
vm.AddEntryAssemblyCommand.CanExecute(null).Should().BeFalse(); |
|
} |
|
|
|
[AvaloniaTest] |
|
public async Task Closing_The_Dialog_Cancels_Work_Still_In_Flight() |
|
{ |
|
var (vm, explorer) = CreateViewModel(); |
|
vm.RefreshCommand.Execute(null); |
|
await Waiters.WaitForAsync(() => vm.Processes.Count == 2); |
|
explorer.ModulesGate = new TaskCompletionSource(); |
|
vm.SelectedProcess = vm.Processes[0]; |
|
await Waiters.WaitForAsync(() => explorer.ModuleCalls.Count == 1); |
|
|
|
vm.CancelAllOperations(); |
|
|
|
explorer.LastModulesToken.IsCancellationRequested.Should().BeTrue( |
|
"a dialog that is gone must not keep a rundown session open"); |
|
} |
|
}
|
|
|