Skip to content

Commit 41c2a78

Browse files
committed
Add selected process summary
1 parent 266834c commit 41c2a78

5 files changed

Lines changed: 580 additions & 1 deletion

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
namespace ThreadPilot.Core.Tests
2+
{
3+
using System.Diagnostics;
4+
using System.Reflection;
5+
using Moq;
6+
using ThreadPilot.Models;
7+
using ThreadPilot.Services;
8+
using ThreadPilot.ViewModels;
9+
10+
public sealed class SelectedProcessSummaryViewModelTests
11+
{
12+
[Fact]
13+
public async Task UpdateAsync_WithNoSelectedProcess_ClearsSummary()
14+
{
15+
var viewModel = new SelectedProcessSummaryViewModel();
16+
17+
await viewModel.UpdateAsync(null);
18+
19+
Assert.False(viewModel.HasSelection);
20+
Assert.Equal("No process selected", viewModel.CurrentProcessStatusText);
21+
Assert.Equal("Memory priority unavailable", viewModel.MemoryPriorityText);
22+
Assert.Equal("No saved rule", viewModel.RuleStatusText);
23+
}
24+
25+
[Fact]
26+
public async Task UpdateAsync_WithSelectedProcess_PopulatesCheapProcessFields()
27+
{
28+
var viewModel = new SelectedProcessSummaryViewModel();
29+
30+
await viewModel.UpdateAsync(CreateProcess("Game.exe", 1234, ProcessPriorityClass.High, 0x3, 512 * 1024 * 1024));
31+
32+
Assert.True(viewModel.HasSelection);
33+
Assert.Equal(1234, viewModel.ProcessId);
34+
Assert.Equal("Game.exe", viewModel.ProcessName);
35+
Assert.Equal(@"C:\Games\Game.exe", viewModel.ExecutablePath);
36+
Assert.Equal("Selected process: Game.exe (PID 1234)", viewModel.ProcessTitle);
37+
Assert.Equal("CPU priority: High", viewModel.CpuPriorityText);
38+
Assert.Equal("Memory: 512 MB", viewModel.MemoryUsageText);
39+
Assert.Equal("Affinity: legacy mask 0x3", viewModel.AffinityText);
40+
}
41+
42+
[Fact]
43+
public async Task UpdateAsync_WhenSelectionChanges_ReplacesSummary()
44+
{
45+
var viewModel = new SelectedProcessSummaryViewModel();
46+
47+
await viewModel.UpdateAsync(CreateProcess("First.exe", 1, ProcessPriorityClass.Normal, 0x1, 1));
48+
await viewModel.UpdateAsync(CreateProcess("Second.exe", 2, ProcessPriorityClass.BelowNormal, 0x2, 2));
49+
50+
Assert.Equal(2, viewModel.ProcessId);
51+
Assert.Equal("Second.exe", viewModel.ProcessName);
52+
Assert.Equal("CPU priority: BelowNormal", viewModel.CpuPriorityText);
53+
Assert.Equal("Affinity: legacy mask 0x2", viewModel.AffinityText);
54+
}
55+
56+
[Fact]
57+
public async Task UpdateAsync_WhenMemoryPriorityReadSucceeds_PopulatesMemoryPriority()
58+
{
59+
var memoryPriority = new Mock<IProcessMemoryPriorityService>(MockBehavior.Strict);
60+
memoryPriority
61+
.Setup(service => service.GetMemoryPriorityAsync(It.IsAny<ProcessModel>()))
62+
.ReturnsAsync(ProcessMemoryPriority.BelowNormal);
63+
var viewModel = new SelectedProcessSummaryViewModel(memoryPriority.Object);
64+
65+
await viewModel.UpdateAsync(CreateProcess());
66+
67+
Assert.Equal(ProcessMemoryPriority.BelowNormal, viewModel.MemoryPriority);
68+
Assert.Equal("Memory priority: BelowNormal", viewModel.MemoryPriorityText);
69+
}
70+
71+
[Fact]
72+
public async Task UpdateAsync_WhenMemoryPriorityUnavailable_ShowsUnavailableWithoutThrowing()
73+
{
74+
var memoryPriority = new Mock<IProcessMemoryPriorityService>(MockBehavior.Strict);
75+
memoryPriority
76+
.Setup(service => service.GetMemoryPriorityAsync(It.IsAny<ProcessModel>()))
77+
.ThrowsAsync(new UnauthorizedAccessException("Access denied"));
78+
var viewModel = new SelectedProcessSummaryViewModel(memoryPriority.Object);
79+
80+
await viewModel.UpdateAsync(CreateProcess());
81+
82+
Assert.Null(viewModel.MemoryPriority);
83+
Assert.Equal("Memory priority unavailable", viewModel.MemoryPriorityText);
84+
}
85+
86+
[Fact]
87+
public void SelectedProcessSummary_HasNoPerformanceMonitoringDependency()
88+
{
89+
var type = typeof(SelectedProcessSummaryViewModel);
90+
91+
var constructorParameters = type
92+
.GetConstructors()
93+
.SelectMany(ctor => ctor.GetParameters())
94+
.Select(parameter => parameter.ParameterType);
95+
var fieldTypes = type
96+
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
97+
.Select(field => field.FieldType);
98+
99+
Assert.DoesNotContain(typeof(IPerformanceMonitoringService), constructorParameters);
100+
Assert.DoesNotContain(typeof(IPerformanceMonitoringService), fieldTypes);
101+
}
102+
103+
[Fact]
104+
public void SelectedProcessSummary_DoesNotOwnTimers()
105+
{
106+
var fieldTypes = typeof(SelectedProcessSummaryViewModel)
107+
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
108+
.Select(field => field.FieldType);
109+
110+
Assert.DoesNotContain(typeof(System.Timers.Timer), fieldTypes);
111+
Assert.DoesNotContain(typeof(System.Threading.Timer), fieldTypes);
112+
}
113+
114+
[Fact]
115+
public async Task UpdateAsync_WhenPersistentRuleMatches_ShowsSavedRule()
116+
{
117+
var store = new Mock<IPersistentProcessRuleStore>(MockBehavior.Strict);
118+
store
119+
.Setup(ruleStore => ruleStore.LoadAsync())
120+
.ReturnsAsync(new[]
121+
{
122+
new PersistentProcessRule
123+
{
124+
Name = "Game rule",
125+
ProcessName = "Game.exe",
126+
IsEnabled = true,
127+
},
128+
});
129+
var viewModel = new SelectedProcessSummaryViewModel(
130+
persistentRuleStore: store.Object,
131+
persistentRuleMatcher: new PersistentProcessRuleMatcher());
132+
133+
await viewModel.UpdateAsync(CreateProcess("Game.exe"));
134+
135+
Assert.True(viewModel.HasThreadPilotRule);
136+
Assert.Equal("Saved rule exists: Game rule", viewModel.RuleStatusText);
137+
}
138+
139+
[Fact]
140+
public async Task UpdateAsync_WhenNoPersistentRuleMatches_ShowsNoSavedRule()
141+
{
142+
var store = new Mock<IPersistentProcessRuleStore>(MockBehavior.Strict);
143+
store
144+
.Setup(ruleStore => ruleStore.LoadAsync())
145+
.ReturnsAsync(new[]
146+
{
147+
new PersistentProcessRule
148+
{
149+
Name = "Other rule",
150+
ProcessName = "Other.exe",
151+
IsEnabled = true,
152+
},
153+
});
154+
var viewModel = new SelectedProcessSummaryViewModel(
155+
persistentRuleStore: store.Object,
156+
persistentRuleMatcher: new PersistentProcessRuleMatcher());
157+
158+
await viewModel.UpdateAsync(CreateProcess("Game.exe"));
159+
160+
Assert.False(viewModel.HasThreadPilotRule);
161+
Assert.Equal("No saved rule", viewModel.RuleStatusText);
162+
}
163+
164+
private static ProcessModel CreateProcess(
165+
string name = "Game.exe",
166+
int processId = 42,
167+
ProcessPriorityClass priority = ProcessPriorityClass.Normal,
168+
long affinity = 0xF,
169+
long memoryUsage = 64 * 1024 * 1024)
170+
=> new()
171+
{
172+
ProcessId = processId,
173+
Name = name,
174+
ExecutablePath = @"C:\Games\Game.exe",
175+
CpuUsage = 12.5,
176+
MemoryUsage = memoryUsage,
177+
Priority = priority,
178+
ProcessorAffinity = affinity,
179+
Classification = ProcessClassification.ForegroundApp,
180+
};
181+
}
182+
}

ViewModels/ProcessViewModel.Behaviors.partial.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
154154

155155
partial void OnSelectedProcessChanged(ProcessModel? value)
156156
{
157+
this.UpdateSelectedProcessSummary(value);
158+
157159
if (value != null && CpuTopology != null)
158160
{
159161
this.HasPendingAffinityEdits = false;
@@ -174,6 +176,13 @@ partial void OnSelectedProcessChanged(ProcessModel? value)
174176
this.systemTrayService.UpdateContextMenu(value?.Name, value != null);
175177
}
176178

179+
private void UpdateSelectedProcessSummary(ProcessModel? process)
180+
{
181+
TaskSafety.FireAndForget(
182+
this.SelectedProcessSummary.UpdateAsync(process, this.StatusMessage, this.HasError),
183+
ex => this.Logger.LogWarning(ex, "Failed to update selected process summary"));
184+
}
185+
177186
private async Task HandleSelectedProcessChangedAsync(ProcessModel value)
178187
{
179188
try
@@ -213,6 +222,7 @@ private async Task HandleSelectedProcessChangedAsync(ProcessModel value)
213222
$"Selected process: {value.Name} (PID: {value.ProcessId}) - " +
214223
$"Priority: {value.Priority}, Affinity: 0x{value.ProcessorAffinity:X}", false);
215224
});
225+
this.UpdateSelectedProcessSummary(value);
216226

217227
// Load current power plan association if available
218228
await this.LoadProcessPowerPlanAssociation(value);
@@ -235,6 +245,7 @@ private async Task HandleSelectedProcessChangedAsync(ProcessModel value)
235245
{
236246
this.SetStatus($"Warning: Could not access process {value.Name} - it may have terminated or require elevated privileges", false);
237247
});
248+
this.UpdateSelectedProcessSummary(value);
238249
}
239250
}
240251

ViewModels/ProcessViewModel.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,10 @@ public ProcessViewModel(
180180
IAffinityApplyService? affinityApplyService = null,
181181
IProcessAffinityApplyCoordinator? processAffinityApplyCoordinator = null,
182182
ICpuTopologyProvider? cpuTopologyProvider = null,
183-
IEnhancedLoggingService? enhancedLoggingService = null)
183+
IEnhancedLoggingService? enhancedLoggingService = null,
184+
IProcessMemoryPriorityService? memoryPriorityService = null,
185+
IPersistentProcessRuleStore? persistentRuleStore = null,
186+
IPersistentProcessRuleMatcher? persistentRuleMatcher = null)
184187
: base(logger, enhancedLoggingService)
185188
{
186189
this.processService = processService ?? throw new ArgumentNullException(nameof(processService));
@@ -202,6 +205,10 @@ public ProcessViewModel(
202205
cpuTopologyProvider,
203206
new CpuSelectionMigrationService(),
204207
NullLogger<ProcessAffinityApplyCoordinator>.Instance);
208+
this.SelectedProcessSummary = new SelectedProcessSummaryViewModel(
209+
memoryPriorityService,
210+
persistentRuleStore,
211+
persistentRuleMatcher);
205212

206213
// Subscribe to topology detection events
207214
this.cpuTopologyService.TopologyDetected += this.OnTopologyDetected;
@@ -223,5 +230,7 @@ public ProcessViewModel(
223230
this.SetupVirtualizedProcessService();
224231
// Note: InitializeAsync() will be called explicitly by MainWindow loading overlay
225232
}
233+
234+
public SelectedProcessSummaryViewModel SelectedProcessSummary { get; }
226235
}
227236
}

0 commit comments

Comments
 (0)