Skip to content

Commit f96a886

Browse files
committed
test: cover low-overhead runtime policies
1 parent 4014c7d commit f96a886

4 files changed

Lines changed: 200 additions & 7 deletions

File tree

Tests/ThreadPilot.Core.Tests/PersistentProcessRuleJsonStoreTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,74 @@ public async Task SaveAsync_WithMissingDirectory_CreatesDirectory()
136136
}
137137
}
138138

139+
[Fact]
140+
public async Task LoadAsync_AfterFirstRead_ReturnsCachedSnapshot()
141+
{
142+
var filePath = CreateTemporaryFilePath();
143+
var store = new PersistentProcessRuleJsonStore(() => filePath);
144+
145+
try
146+
{
147+
await store.SaveAsync([CreateRule("cached", "Cached.exe", ProcessPriorityClass.High)]);
148+
var first = await store.LoadAsync();
149+
await new PersistentProcessRuleJsonStore(() => filePath)
150+
.SaveAsync([CreateRule("external", "External.exe", ProcessPriorityClass.Normal)]);
151+
152+
var second = await store.LoadAsync();
153+
154+
Assert.Same(first, second);
155+
Assert.Equal("cached", Assert.Single(second).Id);
156+
}
157+
finally
158+
{
159+
DeleteFile(filePath);
160+
}
161+
}
162+
163+
[Fact]
164+
public async Task SaveAsync_AfterCachedLoad_UpdatesSnapshot()
165+
{
166+
var filePath = CreateTemporaryFilePath();
167+
var store = new PersistentProcessRuleJsonStore(() => filePath);
168+
169+
try
170+
{
171+
await store.SaveAsync([CreateRule("first", "First.exe", ProcessPriorityClass.High)]);
172+
_ = await store.LoadAsync();
173+
await store.SaveAsync([CreateRule("second", "Second.exe", ProcessPriorityClass.AboveNormal)]);
174+
175+
Assert.Equal("second", Assert.Single(await store.LoadAsync()).Id);
176+
Assert.Equal("second", Assert.Single(await new PersistentProcessRuleJsonStore(() => filePath).LoadAsync()).Id);
177+
}
178+
finally
179+
{
180+
DeleteFile(filePath);
181+
}
182+
}
183+
184+
[Fact]
185+
public async Task ConcurrentSaves_LeaveOneCompleteSnapshot()
186+
{
187+
var filePath = CreateTemporaryFilePath();
188+
var store = new PersistentProcessRuleJsonStore(() => filePath);
189+
var expectedIds = Enumerable.Range(0, 8).Select(index => $"rule-{index}").ToHashSet();
190+
191+
try
192+
{
193+
await Task.WhenAll(expectedIds.Select(id =>
194+
store.SaveAsync([CreateRule(id, $"{id}.exe", ProcessPriorityClass.High)])));
195+
196+
var cached = Assert.Single(await store.LoadAsync());
197+
var persisted = Assert.Single(await new PersistentProcessRuleJsonStore(() => filePath).LoadAsync());
198+
Assert.Contains(cached.Id, expectedIds);
199+
Assert.Equal(cached.Id, persisted.Id);
200+
}
201+
finally
202+
{
203+
DeleteFile(filePath);
204+
}
205+
}
206+
139207
[Fact]
140208
public async Task LoadAsync_WithCorruptJson_ReturnsEmptyList()
141209
{

Tests/ThreadPilot.Core.Tests/PersistentRuleAutoApplyServiceTests.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,50 @@ public async Task ApplyForProcessStartAsync_WhenNoRuleMatches_DoesNotCallRulesEn
126126
Times.Never);
127127
}
128128

129+
[Fact]
130+
public async Task ApplyForProcessStartAsync_WhenNameCannotMatch_DoesNotEnrichProcess()
131+
{
132+
var process = CreateProcess("editor.exe");
133+
process.ExecutablePath = string.Empty;
134+
var rule = CreateRule(processName: "game.exe") with { ExecutablePath = @"C:\Games\Game.exe" };
135+
var engine = CreateEngine(rule, CreateSuccess(rule, process));
136+
var processService = new Mock<IProcessService>(MockBehavior.Strict);
137+
var service = CreateService([rule], engine.Object, processService: processService.Object);
138+
139+
var results = await service.ApplyForProcessStartAsync(process);
140+
141+
Assert.Empty(results);
142+
processService.Verify(
143+
service => service.RefreshProcessInfo(It.IsAny<ProcessModel>()),
144+
Times.Never);
145+
}
146+
147+
[Fact]
148+
public async Task ApplyForProcessStartAsync_WhenPathRuleNameMatches_EnrichesBeforeMatching()
149+
{
150+
var process = CreateProcess();
151+
process.ExecutablePath = string.Empty;
152+
var rule = CreateRule() with { ExecutablePath = @"C:\Games\Game.exe" };
153+
var engine = CreateEngine(rule, CreateSuccess(rule, process));
154+
var processService = new Mock<IProcessService>(MockBehavior.Strict);
155+
processService
156+
.Setup(service => service.RefreshProcessInfo(process))
157+
.Callback(() => process.ExecutablePath = @"C:\Games\Game.exe")
158+
.Returns(Task.CompletedTask);
159+
var service = CreateService([rule], engine.Object, processService: processService.Object);
160+
161+
var results = await service.ApplyForProcessStartAsync(process);
162+
163+
Assert.Single(results);
164+
processService.Verify(service => service.RefreshProcessInfo(process), Times.Once);
165+
engine.Verify(
166+
service => service.ApplyMatchingRulesAsync(
167+
process,
168+
It.IsAny<Predicate<PersistentProcessRule>?>(),
169+
It.IsAny<CancellationToken>()),
170+
Times.Once);
171+
}
172+
129173
[Fact]
130174
public async Task ApplyForProcessStartAsync_DoesNotReapplySameRuleDuringCooldown()
131175
{
@@ -440,7 +484,8 @@ private static PersistentRuleAutoApplyService CreateService(
440484
ApplicationSettingsModel? settings = null,
441485
Func<DateTimeOffset>? nowProvider = null,
442486
IActivityAuditService? audit = null,
443-
IPersistentRulePriorityVerificationService? priorityVerifier = null) =>
487+
IPersistentRulePriorityVerificationService? priorityVerifier = null,
488+
IProcessService? processService = null) =>
444489
new(
445490
new FakePersistentProcessRuleStore(rules),
446491
new PersistentProcessRuleMatcher(),
@@ -450,7 +495,8 @@ private static PersistentRuleAutoApplyService CreateService(
450495
nowProvider ?? (() => DateTimeOffset.UtcNow),
451496
TimeSpan.FromSeconds(30),
452497
audit,
453-
priorityVerifier);
498+
priorityVerifier,
499+
processService);
454500

455501
private static Mock<IPersistentRulesEngine> CreateEngine(
456502
PersistentProcessRule rule,

Tests/ThreadPilot.Core.Tests/ProcessMonitorManagerServiceTests.cs

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,66 @@ public async Task ProcessStarted_WhenPersistentRuleAutoApplyReturnsFailure_DoesN
681681
Times.Never);
682682
}
683683

684+
[Fact]
685+
public async Task ProcessStarted_WithoutRelevantAssociation_DoesNotEnrichOrPersistEventLog()
686+
{
687+
var processMonitor = new FakeProcessMonitorService();
688+
var configuration = new ProcessMonitorConfiguration();
689+
var processService = CreateProcessService();
690+
var enhancedLogger = CreateEnhancedLogger();
691+
var manager = CreateService(
692+
processMonitor,
693+
CreateAssociationService(configuration),
694+
CreatePowerPlanService(),
695+
CreateNotificationService(),
696+
processService,
697+
CreateCoreMaskService(),
698+
CreateAffinityApplyService(),
699+
enhancedLogger: enhancedLogger);
700+
701+
await manager.StartAsync();
702+
enhancedLogger.Invocations.Clear();
703+
processMonitor.RaiseProcessStarted(new ProcessModel { ProcessId = 42, Name = "irrelevant" });
704+
await Task.Delay(100);
705+
706+
processService.Verify(
707+
service => service.RefreshProcessInfo(It.IsAny<ProcessModel>()),
708+
Times.Never);
709+
enhancedLogger.Verify(
710+
logger => logger.LogProcessMonitoringEventAsync(
711+
It.IsAny<string>(),
712+
It.IsAny<string>(),
713+
It.IsAny<int>(),
714+
It.IsAny<string>()),
715+
Times.Never);
716+
}
717+
718+
[Fact]
719+
public async Task ProcessStopped_AlwaysClearsPidCaches()
720+
{
721+
var processMonitor = new FakeProcessMonitorService();
722+
var processService = CreateProcessService();
723+
var coreMaskService = CreateCoreMaskService();
724+
var autoApplyService = CreateAutoApplyService();
725+
var manager = CreateService(
726+
processMonitor,
727+
CreateAssociationService(new ProcessMonitorConfiguration()),
728+
CreatePowerPlanService(),
729+
CreateNotificationService(),
730+
processService,
731+
coreMaskService,
732+
CreateAffinityApplyService(),
733+
autoApplyService);
734+
735+
await manager.StartAsync();
736+
processMonitor.RaiseProcessStopped(new ProcessModel { ProcessId = 73, Name = "short-lived" });
737+
await Task.Delay(100);
738+
739+
autoApplyService.Verify(service => service.MarkProcessExited(73), Times.Once);
740+
coreMaskService.Verify(service => service.UnregisterMaskApplication(73), Times.Once);
741+
processService.Verify(service => service.UntrackProcess(73), Times.Once);
742+
}
743+
684744
[Fact]
685745
public async Task Dispose_CompletesOnBlockingSynchronizationContext()
686746
{
@@ -862,11 +922,7 @@ private sealed class FakeProcessMonitorService : IProcessMonitorService
862922
{
863923
public event EventHandler<ProcessEventArgs>? ProcessStarted;
864924

865-
public event EventHandler<ProcessEventArgs>? ProcessStopped
866-
{
867-
add { }
868-
remove { }
869-
}
925+
public event EventHandler<ProcessEventArgs>? ProcessStopped;
870926

871927
public event EventHandler<MonitoringStatusEventArgs>? MonitoringStatusChanged
872928
{
@@ -917,6 +973,9 @@ public void UpdateSettings()
917973
public void RaiseProcessStarted(ProcessModel process) =>
918974
this.ProcessStarted?.Invoke(this, new ProcessEventArgs(process));
919975

976+
public void RaiseProcessStopped(ProcessModel process) =>
977+
this.ProcessStopped?.Invoke(this, new ProcessEventArgs(process));
978+
920979
public void Dispose()
921980
{
922981
this.DisposeCalls++;

Tests/ThreadPilot.Core.Tests/ProcessServiceTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,26 @@ public void IsPassiveProcessAccessException_ReturnsFalse_ForUnrelatedException()
347347
Assert.False(result);
348348
}
349349

350+
[Fact]
351+
public async Task GetProcessesByNameAsync_ReturnsMaterializedModels()
352+
{
353+
var profilesDirectory = CreateTemporaryDirectory();
354+
var service = CreateService(profilesDirectory);
355+
using var currentProcess = Process.GetCurrentProcess();
356+
357+
try
358+
{
359+
var results = await service.GetProcessesByNameAsync(currentProcess.ProcessName);
360+
361+
var models = Assert.IsType<List<ProcessModel>>(results);
362+
Assert.Contains(models, process => process.ProcessId == currentProcess.Id);
363+
}
364+
finally
365+
{
366+
DeleteDirectory(profilesDirectory);
367+
}
368+
}
369+
350370
[Fact]
351371
public void TrackPriorityChange_PreservesOriginalPriority()
352372
{

0 commit comments

Comments
 (0)