Skip to content

Commit 37b5f1f

Browse files
committed
feat: add async registry ops, tests, and UI/perf improvements
- Refactored BaseFeature and registry toggle logic to be async and non-blocking - Added integration/unit tests for BaseFeature and OptimizationService - Improved registry value conversion for more bool formats - Enhanced error handling and async startup in App.xaml.cs - Made optimization preloading async to avoid UI blocking - Updated ApplyResult to use ErrorMessage for clarity - Refactored revert steps to use async/await and correct execution order - Batched file checks in UpdateOptimizationStateAsync for performance - Hardened config file validation against malformed JSON - Added ProgressBarVisibilityConverter and improved progress bar logic - Enabled virtualization for disk volume lists in dashboard - Added Simplified Chinese language option - Miscellaneous code cleanups, logging, and thread-safety fixes
1 parent 5c69b79 commit 37b5f1f

24 files changed

Lines changed: 999 additions & 294 deletions
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System.Collections.ObjectModel;
2+
using System.Threading.Channels;
3+
using System.Threading.Tasks;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Win32;
6+
using optimizerDuck.Domain.Features.Models;
7+
using optimizerDuck.Domain.Optimizations.Models.Services;
8+
using optimizerDuck.Services.OptimizationServices;
9+
using Xunit;
10+
11+
namespace optimizerDuck.Test.Domain.Features;
12+
13+
public class BaseFeatureTests : IDisposable
14+
{
15+
private const string TestKeyPath = @"HKCU\Software\TestOptimizerDuckFeatures";
16+
17+
private class TestFeature : BaseFeature
18+
{
19+
// FeatureKey is not virtual, so we don't override it
20+
// The test will use the actual class name
21+
22+
protected override IEnumerable<RegistryToggle> RegistryToggles =>
23+
[
24+
new()
25+
{
26+
Path = TestKeyPath,
27+
Name = "TestValue",
28+
OnValue = 1,
29+
OffValue = 0,
30+
ValueKind = RegistryValueKind.DWord,
31+
},
32+
];
33+
}
34+
35+
public BaseFeatureTests()
36+
{
37+
CleanupTestKeys();
38+
}
39+
40+
public void Dispose()
41+
{
42+
CleanupTestKeys();
43+
}
44+
45+
private static void CleanupTestKeys()
46+
{
47+
try
48+
{
49+
using var hkcu = Registry.CurrentUser;
50+
hkcu.DeleteSubKeyTree(@"Software\TestOptimizerDuckFeatures", false);
51+
}
52+
catch
53+
{
54+
// Ignore if it doesn't exist
55+
}
56+
}
57+
58+
[Fact]
59+
public async Task EnableAsync_WritesCorrectValueToRegistry()
60+
{
61+
var feature = new TestFeature { OwnerType = typeof(TestFeature) };
62+
63+
await feature.EnableAsync();
64+
65+
var value = RegistryService.Read<int>(new RegistryItem(TestKeyPath, "TestValue"));
66+
Assert.Equal(1, value);
67+
}
68+
69+
[Fact]
70+
public async Task DisableAsync_WritesCorrectValueToRegistry()
71+
{
72+
var feature = new TestFeature { OwnerType = typeof(TestFeature) };
73+
74+
await feature.DisableAsync();
75+
76+
var value = RegistryService.Read<int>(new RegistryItem(TestKeyPath, "TestValue"));
77+
Assert.Equal(0, value);
78+
}
79+
80+
[Fact]
81+
public async Task GetStateAsync_ReturnsCorrectState()
82+
{
83+
var feature = new TestFeature { OwnerType = typeof(TestFeature) };
84+
85+
// Initially should be false (value doesn't exist)
86+
var initialState = await feature.GetStateAsync();
87+
Assert.False(initialState);
88+
89+
// Enable
90+
await feature.EnableAsync();
91+
var enabledState = await feature.GetStateAsync();
92+
Assert.True(enabledState);
93+
94+
// Disable
95+
await feature.DisableAsync();
96+
var disabledState = await feature.GetStateAsync();
97+
Assert.False(disabledState);
98+
}
99+
100+
[Fact]
101+
public async Task ToggleOperations_DoesNotBlockCallingThread()
102+
{
103+
var feature = new TestFeature { OwnerType = typeof(TestFeature) };
104+
105+
// Measure time - should be fast since it's offloaded to thread pool
106+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
107+
108+
await feature.EnableAsync();
109+
await feature.DisableAsync();
110+
111+
stopwatch.Stop();
112+
113+
// Should complete quickly (not blocking on UI thread equivalent)
114+
Assert.True(stopwatch.ElapsedMilliseconds < 1000,
115+
$"Operations took {stopwatch.ElapsedMilliseconds}ms, expected < 1000ms");
116+
}
117+
118+
[Fact]
119+
public async Task MultipleToggles_ExecutesSequentiallyWithoutCorruption()
120+
{
121+
var feature = new TestFeature { OwnerType = typeof(TestFeature) };
122+
123+
// Perform multiple rapid toggles
124+
for (int i = 0; i < 10; i++)
125+
{
126+
await feature.EnableAsync();
127+
var enabledValue = RegistryService.Read<int>(new RegistryItem(TestKeyPath, "TestValue"));
128+
Assert.Equal(1, enabledValue);
129+
130+
await feature.DisableAsync();
131+
var disabledValue = RegistryService.Read<int>(new RegistryItem(TestKeyPath, "TestValue"));
132+
Assert.Equal(0, disabledValue);
133+
}
134+
135+
// Final state should be disabled
136+
var finalState = await feature.GetStateAsync();
137+
Assert.False(finalState);
138+
}
139+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
using Newtonsoft.Json.Linq;
9+
using optimizerDuck.Common.Helpers;
10+
using optimizerDuck.Domain.Abstractions;
11+
using optimizerDuck.Domain.Execution;
12+
using optimizerDuck.Domain.Optimizations.Models;
13+
using optimizerDuck.Domain.Revert;
14+
using optimizerDuck.Domain.UI;
15+
using optimizerDuck.Services;
16+
using optimizerDuck.Services.Managers;
17+
using Xunit;
18+
19+
namespace optimizerDuck.Test.Services;
20+
21+
public class OptimizationServiceIntegrationTests : IDisposable
22+
{
23+
private class PartialFailureOptimization : IOptimization
24+
{
25+
private int _stepCount;
26+
private readonly bool _shouldFail;
27+
28+
public Guid Id { get; } = Guid.NewGuid();
29+
public OptimizationRisk Risk => OptimizationRisk.Safe;
30+
public string OptimizationKey => "PartialFailureTest";
31+
public string Name => "Partial Failure Test";
32+
public string ShortDescription => "Tests partial failure scenarios";
33+
public OptimizationState State { get; set; } = new();
34+
35+
public PartialFailureOptimization(bool shouldFail = false)
36+
{
37+
_shouldFail = shouldFail;
38+
}
39+
40+
public Task<ApplyResult> ApplyAsync(
41+
IProgress<ProcessingProgress> progress,
42+
OptimizationContext context
43+
)
44+
{
45+
var results = new List<OperationStepResult>();
46+
_stepCount = 0;
47+
48+
// Step 1: Always succeeds
49+
ExecutionScope.RecordStep(
50+
"Test",
51+
"Step 1",
52+
true,
53+
new TestRevertStep { StepId = 1 }
54+
);
55+
_stepCount++;
56+
57+
// Step 2: May fail
58+
if (_shouldFail)
59+
{
60+
ExecutionScope.RecordStep(
61+
"Test",
62+
"Step 2",
63+
false,
64+
null,
65+
"Simulated failure"
66+
);
67+
_stepCount++;
68+
}
69+
70+
// Step 3: Only executes if step 2 succeeded
71+
if (!_shouldFail)
72+
{
73+
ExecutionScope.RecordStep(
74+
"Test",
75+
"Step 3",
76+
true,
77+
new TestRevertStep { StepId = 3 }
78+
);
79+
_stepCount++;
80+
}
81+
82+
var success = !_shouldFail;
83+
return Task.FromResult(
84+
success
85+
? ApplyResult.True()
86+
: ApplyResult.False("Step 2 failed intentionally")
87+
);
88+
}
89+
}
90+
91+
private class TestRevertStep : IRevertStep
92+
{
93+
public int StepId { get; init; }
94+
public string Type => "Test";
95+
public string Description => $"Revert step {StepId}";
96+
97+
public Task<bool> ExecuteAsync()
98+
{
99+
// Simulate revert operation
100+
return Task.FromResult(true);
101+
}
102+
103+
public JObject ToData()
104+
{
105+
return new JObject { ["StepId"] = StepId };
106+
}
107+
}
108+
109+
public OptimizationServiceIntegrationTests()
110+
{
111+
// Ensure clean revert directory
112+
if (Directory.Exists(Shared.RevertDirectory))
113+
{
114+
foreach (var file in Directory.GetFiles(Shared.RevertDirectory))
115+
{
116+
try
117+
{
118+
File.Delete(file);
119+
}
120+
catch
121+
{
122+
// Ignore
123+
}
124+
}
125+
}
126+
}
127+
128+
public void Dispose()
129+
{
130+
// Clean up test files
131+
if (Directory.Exists(Shared.RevertDirectory))
132+
{
133+
foreach (var file in Directory.GetFiles(Shared.RevertDirectory))
134+
{
135+
try
136+
{
137+
File.Delete(file);
138+
}
139+
catch
140+
{
141+
// Ignore
142+
}
143+
}
144+
}
145+
}
146+
147+
[Fact]
148+
public async Task ApplyAsync_WithSuccess_SavesRevertDataCorrectly()
149+
{
150+
var loggerFactory = NullLoggerFactory.Instance;
151+
var revertManager = new RevertManager(NullLogger<RevertManager>.Instance, loggerFactory);
152+
var systemInfoService = new SystemInfoService(NullLogger<SystemInfoService>.Instance);
153+
var streamService = new StreamService(NullLogger<StreamService>.Instance);
154+
155+
var optimizationService = new OptimizationService(
156+
revertManager,
157+
loggerFactory,
158+
systemInfoService,
159+
streamService,
160+
null!,
161+
NullLogger<OptimizationService>.Instance
162+
);
163+
164+
var optimization = new PartialFailureOptimization(shouldFail: false);
165+
var progress = new Progress<ProcessingProgress>();
166+
167+
var result = await optimizationService.ApplyAsync(optimization, progress);
168+
169+
Assert.Equal(OptimizationSuccessResult.Success, result.Status);
170+
171+
// Verify revert data was saved
172+
var revertData = await RevertManager.GetRevertDataAsync(optimization.Id);
173+
Assert.NotNull(revertData);
174+
Assert.Equal(2, revertData.Steps.Count); // 2 successful steps
175+
}
176+
177+
[Fact]
178+
public async Task UpdateOptimizationStateAsync_WithMissingData_HandlesGracefully()
179+
{
180+
var optimizations = new IOptimization[]
181+
{
182+
new PartialFailureOptimization(shouldFail: false),
183+
new PartialFailureOptimization(shouldFail: true),
184+
};
185+
186+
// These optimizations have never been applied, so no revert data exists
187+
await OptimizationService.UpdateOptimizationStateAsync(optimizations);
188+
189+
// All should be marked as not applied
190+
Assert.False(optimizations[0].State.IsApplied);
191+
Assert.False(optimizations[1].State.IsApplied);
192+
}
193+
194+
[Fact]
195+
public async Task RetryFailedStepsAsync_WithRetryableSteps_Succeeds()
196+
{
197+
var failedSteps = new List<OperationStepResult>
198+
{
199+
new()
200+
{
201+
Index = 1,
202+
Name = "Test",
203+
Description = "Failed step",
204+
Success = false,
205+
Error = "Temporary failure",
206+
RetryAction = () => Task.FromResult(true),
207+
},
208+
};
209+
210+
var logger = NullLogger.Instance;
211+
var recoveredSteps = await OptimizationService.RetryFailedStepsAsync(
212+
failedSteps,
213+
false,
214+
logger,
215+
null
216+
);
217+
218+
// Should succeed on retry
219+
Assert.Empty(recoveredSteps);
220+
}
221+
222+
[Fact]
223+
public async Task RetryFailedStepsAsync_WithNonRetryableSteps_RemainsFailed()
224+
{
225+
var failedSteps = new List<OperationStepResult>
226+
{
227+
new()
228+
{
229+
Index = 1,
230+
Name = "Test",
231+
Description = "Failed step",
232+
Success = false,
233+
Error = "Permanent failure",
234+
RetryAction = null, // No retry action
235+
},
236+
};
237+
238+
var logger = NullLogger.Instance;
239+
var remainingFailed = await OptimizationService.RetryFailedStepsAsync(
240+
failedSteps,
241+
false,
242+
logger,
243+
null
244+
);
245+
246+
// Should remain failed
247+
Assert.Single(remainingFailed);
248+
}
249+
}

0 commit comments

Comments
 (0)