Initial commit: SpdUp Windows Gaming Booster v1.0.0

- .NET 8 WPF app with full MVVM architecture
- One-click boost: CPU, RAM, GPU, network, services, timer resolution
- Real-time system monitor (LibreHardwareMonitor)
- Auto game detection (30+ games)
- RAM optimizer with kernel-level cleanup
- Network optimizer (TCP registry tweaks)
- Service manager, process manager, shader cache cleaner
- Desktop widget overlay
- Debug log viewer
- Custom dark gaming theme with trans pride colors
- 84 unit tests (xUnit)
- Inno Setup installer script
- Landing page website
This commit is contained in:
Phil-icyou
2026-03-08 17:34:45 +01:00
commit 19ba425e79
68 changed files with 6176 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
using System.Globalization;
using System.Windows;
using System.Windows.Media;
using SpdUp.Converters;
namespace SpdUp.Tests;
/// <summary>
/// Tests for WPF value converters.
/// </summary>
public class ConverterTests
{
private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
// ── BoolToVisibilityConverter ─────────────────────────────
[Fact]
public void BoolToVisibility_True_ReturnsVisible()
{
var conv = new BoolToVisibilityConverter();
var result = conv.Convert(true, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
[Fact]
public void BoolToVisibility_False_ReturnsCollapsed()
{
var conv = new BoolToVisibilityConverter();
var result = conv.Convert(false, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void BoolToVisibility_ConvertBack_Visible_ReturnsTrue()
{
var conv = new BoolToVisibilityConverter();
var result = conv.ConvertBack(Visibility.Visible, typeof(bool), null!, Culture);
Assert.Equal(true, result);
}
[Fact]
public void BoolToVisibility_ConvertBack_Collapsed_ReturnsFalse()
{
var conv = new BoolToVisibilityConverter();
var result = conv.ConvertBack(Visibility.Collapsed, typeof(bool), null!, Culture);
Assert.Equal(false, result);
}
// ── InverseBoolToVisibilityConverter ──────────────────────
[Fact]
public void InverseBoolToVisibility_True_ReturnsCollapsed()
{
var conv = new InverseBoolToVisibilityConverter();
var result = conv.Convert(true, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void InverseBoolToVisibility_False_ReturnsVisible()
{
var conv = new InverseBoolToVisibilityConverter();
var result = conv.Convert(false, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
// ── PercentToColorConverter ──────────────────────────────
[Theory]
[InlineData(95f, 255, 69, 58)] // red for > 90
[InlineData(91f, 255, 69, 58)] // red at boundary
[InlineData(80f, 255, 159, 10)] // orange for > 70
[InlineData(71f, 255, 159, 10)] // orange at boundary
[InlineData(50f, 48, 209, 88)] // green for <= 70
[InlineData(0f, 48, 209, 88)] // green at zero
public void PercentToColor_ReturnsCorrectColor(float pct, byte r, byte g, byte b)
{
var conv = new PercentToColorConverter();
var result = conv.Convert(pct, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(r, result.Color.R);
Assert.Equal(g, result.Color.G);
Assert.Equal(b, result.Color.B);
}
[Fact]
public void PercentToColor_ConvertBack_Throws()
{
var conv = new PercentToColorConverter();
Assert.Throws<NotSupportedException>(() =>
conv.ConvertBack(null!, typeof(float), null!, Culture));
}
// ── NullToVisibilityConverter ────────────────────────────
[Fact]
public void NullToVisibility_Null_ReturnsCollapsed()
{
var conv = new NullToVisibilityConverter();
var result = conv.Convert(null!, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void NullToVisibility_NotNull_ReturnsVisible()
{
var conv = new NullToVisibilityConverter();
var result = conv.Convert("something", typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
// ── FloatFormatConverter ─────────────────────────────────
[Fact]
public void FloatFormat_DefaultFormat_OneDecimal()
{
var conv = new FloatFormatConverter();
var result = (string)conv.Convert(42.567f, typeof(string), null!, CultureInfo.InvariantCulture);
// Converter uses current culture internally, so parse back to float
Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed));
Assert.Equal(42.6f, parsed, 0.05f);
}
[Fact]
public void FloatFormat_CustomFormat()
{
var conv = new FloatFormatConverter();
var result = (string)conv.Convert(42.567f, typeof(string), "F2", CultureInfo.InvariantCulture);
Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed));
Assert.Equal(42.57f, parsed, 0.005f);
}
// ── BoostButtonTextConverter ─────────────────────────────
[Fact]
public void BoostButtonText_True_ReturnsStopText()
{
var conv = new BoostButtonTextConverter();
var result = conv.Convert(true, typeof(string), null!, Culture);
Assert.Contains("STOP", result as string ?? "");
}
[Fact]
public void BoostButtonText_False_ReturnsBoostText()
{
var conv = new BoostButtonTextConverter();
var result = conv.Convert(false, typeof(string), null!, Culture);
Assert.Contains("BOOST", result as string ?? "");
}
// ── BoostButtonColorConverter ────────────────────────────
[Fact]
public void BoostButtonColor_True_ReturnsRed()
{
var conv = new BoostButtonColorConverter();
var result = conv.Convert(true, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(255, result.Color.R);
Assert.Equal(69, result.Color.G);
}
[Fact]
public void BoostButtonColor_False_ReturnsGreen()
{
var conv = new BoostButtonColorConverter();
var result = conv.Convert(false, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(48, result.Color.R);
Assert.Equal(209, result.Color.G);
}
}
+1
View File
@@ -0,0 +1 @@
global using Xunit;
+117
View File
@@ -0,0 +1,117 @@
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for the Logger class.
/// </summary>
public class LoggerTests
{
[Fact]
public void Info_FiresLogWrittenEvent()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Info("test info message");
Assert.NotNull(received);
Assert.Contains("[INFO]", received);
Assert.Contains("test info message", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Warn_FiresLogWrittenEventWithWarnLevel()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Warn("test warning");
Assert.NotNull(received);
Assert.Contains("[WARN]", received);
Assert.Contains("test warning", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Error_IncludesExceptionDetails()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Error("something failed", new InvalidOperationException("bad state"));
Assert.NotNull(received);
Assert.Contains("[ERROR]", received);
Assert.Contains("something failed", received);
Assert.Contains("InvalidOperationException", received);
Assert.Contains("bad state", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Error_WithoutException_DoesNotContainPipe()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Error("simple error");
Assert.NotNull(received);
Assert.Contains("[ERROR]", received);
Assert.Contains("simple error", received);
Assert.DoesNotContain("|", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void LogWritten_IncludesTimestamp()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Info("timestamp check");
Assert.NotNull(received);
// Format: [yyyy-MM-dd HH:mm:ss.fff]
Assert.Matches(@"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
}
+224
View File
@@ -0,0 +1,224 @@
using SpdUp.Models;
namespace SpdUp.Tests;
/// <summary>
/// Tests for the model classes.
/// </summary>
public class ModelTests
{
// ── SystemMetrics ────────────────────────────────────────
[Fact]
public void SystemMetrics_RamUsagePercent_CalculatesCorrectly()
{
var m = new SystemMetrics { RamUsageMB = 8000, RamTotalMB = 16000 };
Assert.Equal(50f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_RamUsagePercent_ZeroTotal_ReturnsZero()
{
var m = new SystemMetrics { RamUsageMB = 100, RamTotalMB = 0 };
Assert.Equal(0f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_RamUsagePercent_FullRam()
{
var m = new SystemMetrics { RamUsageMB = 16000, RamTotalMB = 16000 };
Assert.Equal(100f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_DefaultTimestamp_IsNow()
{
var before = DateTime.Now.AddSeconds(-1);
var m = new SystemMetrics();
var after = DateTime.Now.AddSeconds(1);
Assert.InRange(m.Timestamp, before, after);
}
// ── GameEntry ────────────────────────────────────────────
[Fact]
public void GameEntry_ProcessNameFromPath_ExtractsFileName()
{
var result = GameEntry.ProcessNameFromPath(@"C:\Games\Cyberpunk2077\bin\Cyberpunk2077.exe");
Assert.Equal("Cyberpunk2077", result);
}
[Fact]
public void GameEntry_ProcessNameFromPath_HandlesForwardSlashes()
{
var result = GameEntry.ProcessNameFromPath("C:/Games/Game.exe");
Assert.Equal("Game", result);
}
[Fact]
public void GameEntry_ProcessNameFromPath_JustFileName()
{
var result = GameEntry.ProcessNameFromPath("myapp.exe");
Assert.Equal("myapp", result);
}
[Fact]
public void GameEntry_DefaultValues()
{
var g = new GameEntry();
Assert.Equal(string.Empty, g.Name);
Assert.Equal(string.Empty, g.ExecutablePath);
Assert.Equal("Manual", g.LauncherSource);
Assert.True(g.AutoBoost);
Assert.Null(g.IconPath);
}
[Fact]
public void BoostProfile_DefaultValues()
{
var p = new BoostProfile();
Assert.True(p.CleanRam);
Assert.True(p.SetHighPriority);
Assert.True(p.ReduceTimerResolution);
Assert.True(p.StopServices);
Assert.False(p.OptimizeNetwork);
}
// ── ProcessEntry ─────────────────────────────────────────
[Fact]
public void ProcessEntry_Properties()
{
var p = new ProcessEntry
{
Pid = 1234,
Name = "chrome",
WindowTitle = "Google Chrome",
CpuPercent = 12.5f,
MemoryMB = 350f,
PriorityClass = "High",
IsSystem = false
};
Assert.Equal(1234, p.Pid);
Assert.Equal("chrome", p.Name);
Assert.Equal("Google Chrome", p.WindowTitle);
Assert.Equal(12.5f, p.CpuPercent);
Assert.Equal(350f, p.MemoryMB);
Assert.Equal("High", p.PriorityClass);
Assert.False(p.IsSystem);
}
[Fact]
public void ProcessEntry_DefaultPriority_IsNormal()
{
var p = new ProcessEntry { Pid = 1, Name = "test" };
Assert.Equal("Normal", p.PriorityClass);
}
// ── ServiceEntry ─────────────────────────────────────────
[Fact]
public void ServiceEntry_DefaultStatus_IsUnknown()
{
var s = new ServiceEntry();
Assert.Equal("Unknown", s.Status);
Assert.False(s.WasRunningBeforeBoost);
}
[Fact]
public void ServiceEntry_Properties()
{
var s = new ServiceEntry
{
ServiceName = "WSearch",
DisplayName = "Windows Search",
Status = "Running",
WasRunningBeforeBoost = true,
Description = "Full-text search"
};
Assert.Equal("WSearch", s.ServiceName);
Assert.Equal("Windows Search", s.DisplayName);
Assert.Equal("Running", s.Status);
Assert.True(s.WasRunningBeforeBoost);
}
// ── AppSettings ──────────────────────────────────────────
[Fact]
public void AppSettings_DefaultValues()
{
var s = new AppSettings();
Assert.True(s.MinimizeToTray);
Assert.False(s.StartWithWindows);
Assert.True(s.AutoDetectGames);
Assert.True(s.AutoBoostOnGameLaunch);
Assert.False(s.ShowWidget);
Assert.Equal("8.8.8.8", s.PingTarget);
Assert.Equal("1.1.1.1", s.PreferredDns);
Assert.Equal(1000, s.MonitoringIntervalMs);
Assert.Contains("SysMain", s.ServicesToStop);
Assert.Contains("WSearch", s.ServicesToStop);
}
[Fact]
public void AppSettings_WidgetDefaults()
{
var s = new AppSettings();
Assert.True(s.WidgetCompactMode);
Assert.Equal(100, s.WidgetLeft);
Assert.Equal(100, s.WidgetTop);
Assert.Equal(0.85, s.WidgetOpacity);
Assert.Equal(1000, s.WidgetUpdateIntervalMs);
Assert.False(s.WidgetClickThrough);
}
// ── ShaderCleanResult ────────────────────────────────────
[Fact]
public void ShaderCleanResult_MBFreed_Calculation()
{
var r = new SpdUp.Services.ShaderCleanResult(10, 10_485_760); // 10 MB
Assert.Equal(10.0, r.MBFreed, 1);
}
[Fact]
public void ShaderCleanResult_ZeroBytes()
{
var r = new SpdUp.Services.ShaderCleanResult(0, 0);
Assert.Equal(0.0, r.MBFreed);
Assert.Equal(0, r.FilesDeleted);
}
// ── RamCleanupResult ─────────────────────────────────────
[Fact]
public void RamCleanupResult_RecordWithSyntax()
{
var r = new SpdUp.Services.RamCleanupResult(50, 5, 0);
var r2 = r with { StandbyPurged = 1 };
Assert.Equal(50, r2.TrimmedCount);
Assert.Equal(5, r2.FailedCount);
Assert.Equal(1, r2.StandbyPurged);
}
// ── BoostResult ──────────────────────────────────────────
[Fact]
public void BoostResult_Properties()
{
var r = new SpdUp.Services.BoostResult(true, "Boost activated");
Assert.True(r.Success);
Assert.Equal("Boost activated", r.Message);
}
[Fact]
public void BoostResult_Failure()
{
var r = new SpdUp.Services.BoostResult(false, "Error occurred");
Assert.False(r.Success);
}
}
+93
View File
@@ -0,0 +1,93 @@
using System.ComponentModel;
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for ObservableObject base class.
/// </summary>
public class ObservableObjectTests
{
private sealed class TestObservable : ObservableObject
{
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _count;
public int Count
{
get => _count;
set => SetProperty(ref _count, value);
}
}
[Fact]
public void SetProperty_NewValue_RaisesPropertyChanged()
{
var obj = new TestObservable();
string? changedProp = null;
obj.PropertyChanged += (_, e) => changedProp = e.PropertyName;
obj.Name = "hello";
Assert.Equal("Name", changedProp);
Assert.Equal("hello", obj.Name);
}
[Fact]
public void SetProperty_SameValue_DoesNotRaisePropertyChanged()
{
var obj = new TestObservable { Name = "hello" };
bool raised = false;
obj.PropertyChanged += (_, _) => raised = true;
obj.Name = "hello";
Assert.False(raised);
}
[Fact]
public void SetProperty_ReturnsTrue_WhenValueChanged()
{
var obj = new TestObservable();
string? changedProp = null;
obj.PropertyChanged += (_, e) => changedProp = e.PropertyName;
obj.Count = 42;
Assert.Equal("Count", changedProp);
Assert.Equal(42, obj.Count);
}
[Fact]
public void SetProperty_ReturnsFalse_WhenValueUnchanged()
{
var obj = new TestObservable { Count = 5 };
bool raised = false;
obj.PropertyChanged += (_, _) => raised = true;
obj.Count = 5;
Assert.False(raised);
}
[Fact]
public void MultipleProperties_OnlyNotifyChanged()
{
var obj = new TestObservable();
var notifications = new List<string>();
obj.PropertyChanged += (_, e) => notifications.Add(e.PropertyName!);
obj.Name = "a";
obj.Count = 1;
obj.Name = "a"; // same no notification
obj.Count = 2;
Assert.Equal(3, notifications.Count);
Assert.Equal(["Name", "Count", "Count"], notifications);
}
}
+99
View File
@@ -0,0 +1,99 @@
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for RelayCommand and RelayCommand&lt;T&gt;.
/// </summary>
public class RelayCommandTests
{
[Fact]
public void Execute_InvokesAction()
{
bool executed = false;
var cmd = new RelayCommand(() => executed = true);
cmd.Execute(null);
Assert.True(executed);
}
[Fact]
public void Execute_WithParameter_InvokesParameterizedAction()
{
object? received = null;
var cmd = new RelayCommand(p => received = p);
cmd.Execute("test");
Assert.Equal("test", received);
}
[Fact]
public void CanExecute_DefaultsToTrue()
{
var cmd = new RelayCommand(() => { });
Assert.True(cmd.CanExecute(null));
}
[Fact]
public void CanExecute_RespectsCanExecuteFunc()
{
var cmd = new RelayCommand(() => { }, () => false);
Assert.False(cmd.CanExecute(null));
}
[Fact]
public void CanExecute_ParameterizedPredicate()
{
var cmd = new RelayCommand(_ => { }, p => p is "go");
Assert.True(cmd.CanExecute("go"));
Assert.False(cmd.CanExecute("stop"));
Assert.False(cmd.CanExecute(null));
}
[Fact]
public void Constructor_ThrowsOnNullAction()
{
Assert.Throws<ArgumentNullException>(() => new RelayCommand((Action<object?>)null!));
}
// ── Generic RelayCommand<T> ──────────────────────────────
[Fact]
public void Generic_Execute_InvokesWithTypedParameter()
{
string? received = null;
var cmd = new RelayCommand<string>(s => received = s);
cmd.Execute("hello");
Assert.Equal("hello", received);
}
[Fact]
public void Generic_CanExecute_DefaultsToTrue()
{
var cmd = new RelayCommand<int>(_ => { });
Assert.True(cmd.CanExecute(42));
}
[Fact]
public void Generic_CanExecute_RespectsFunc()
{
var cmd = new RelayCommand<int>(_ => { }, n => n > 0);
Assert.True(cmd.CanExecute(5));
Assert.False(cmd.CanExecute(-1));
}
[Fact]
public void Generic_Constructor_ThrowsOnNullAction()
{
Assert.Throws<ArgumentNullException>(() => new RelayCommand<string>(null!));
}
}
+150
View File
@@ -0,0 +1,150 @@
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.Tests;
/// <summary>
/// Tests for services that can be tested without admin/system access.
/// </summary>
public class ServiceTests
{
// ── NetworkService.PingAsync ──────────────────────────────
[Fact]
public async Task PingAsync_Localhost_ReturnsPositive()
{
var ms = await NetworkService.PingAsync("127.0.0.1", 2000);
Assert.True(ms >= 0, $"Ping to localhost should succeed, got {ms}");
}
[Fact]
public async Task PingAsync_InvalidHost_ReturnsNegative()
{
var ms = await NetworkService.PingAsync("0.0.0.0", 500);
// 0.0.0.0 typically fails or returns -1
// On some machines it may succeed — accept either
Assert.True(ms >= -1);
}
// ── GameDetectionService ─────────────────────────────────
[Fact]
public void GameDetectionService_InitialState_NoGameDetected()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
Assert.Null(gds.CurrentGameProcess);
}
[Fact]
public void GameDetectionService_AddGameProcess()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
gds.AddGameProcess("TestGame");
// AddGameProcess should not throw
}
[Fact]
public void GameDetectionService_StartAndStop_DoesNotThrow()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
gds.Start(60000); // very long interval so it doesn't actually poll during test
gds.Stop();
}
[Fact]
public void GameDetectionService_Dispose_DoesNotThrow()
{
var settings = new AppSettings();
var gds = new GameDetectionService(settings);
gds.Dispose();
// Double dispose should not throw
gds.Dispose();
}
// ── OptimizationService ──────────────────────────────────
[Fact]
public void OptimizationService_InitialState()
{
var settings = new AppSettings();
var svc = new OptimizationService(settings);
Assert.False(svc.IsBoosted);
Assert.Null(svc.BoostedProcessName);
}
[Fact]
public void OptimizationService_Events_CanSubscribe()
{
var settings = new AppSettings();
var svc = new OptimizationService(settings);
bool activated = false;
bool deactivated = false;
svc.BoostActivated += _ => activated = true;
svc.BoostDeactivated += () => deactivated = true;
// Just verify event subscription doesn't throw
Assert.False(activated);
Assert.False(deactivated);
}
// ── ServiceManagementService ─────────────────────────────
[Fact]
public void KnownNonCriticalServices_ContainsExpectedEntries()
{
var known = ServiceManagementService.KnownNonCriticalServices;
Assert.True(known.ContainsKey("SysMain"));
Assert.True(known.ContainsKey("WSearch"));
Assert.True(known.ContainsKey("DiagTrack"));
Assert.True(known.ContainsKey("XblAuthManager"));
Assert.True(known.Count >= 10, "Should have at least 10 known services");
}
[Fact]
public void GetServiceList_NonExistentService_ReturnsNotFound()
{
var entries = ServiceManagementService.GetServiceList(new[] { "SpdUpFakeService12345" });
Assert.Single(entries);
Assert.Equal("NotFound", entries[0].Status);
Assert.Equal("SpdUpFakeService12345", entries[0].ServiceName);
}
[Fact]
public void GetServiceList_EmptyInput_ReturnsEmpty()
{
var entries = ServiceManagementService.GetServiceList(Array.Empty<string>());
Assert.Empty(entries);
}
// ── ShaderCleanResult ────────────────────────────────────
[Fact]
public void ShaderCleanResult_MBFreed_LargeValue()
{
var r = new ShaderCleanResult(100, 1_073_741_824); // 1 GB
Assert.Equal(1024.0, r.MBFreed, 0.1);
}
// ── RamOptimizationService ───────────────────────────────
[Fact]
public void RamOptimizationService_GetMemoryStatus_ReturnsValidValues()
{
var (totalMB, availMB, loadPercent) = RamOptimizationService.GetMemoryStatus();
Assert.True(totalMB > 0, "Total RAM should be > 0");
Assert.True(availMB > 0, "Available RAM should be > 0");
Assert.True(availMB <= totalMB, "Available should be <= total");
Assert.InRange(loadPercent, (uint)1, (uint)100);
}
}
+30
View File
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SpdUp.csproj" />
</ItemGroup>
</Project>
+157
View File
@@ -0,0 +1,157 @@
using System.Collections.Specialized;
using SpdUp.Core;
using SpdUp.ViewModels;
namespace SpdUp.Tests;
/// <summary>
/// Tests for DebugLogViewModel.
/// Uses a helper to run on STA thread for WPF Dispatcher compatibility.
/// </summary>
public class DebugLogViewModelTests
{
private static void RunOnSta(Action action)
{
Exception? caught = null;
var thread = new Thread(() =>
{
try { action(); }
catch (Exception ex) { caught = ex; }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (caught is not null)
throw new Xunit.Sdk.XunitException($"STA test failed: {caught.Message}", caught);
}
[Fact]
public void Initial_LogLines_IsEmpty()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.Empty(vm.LogLines);
});
}
[Fact]
public void AutoScroll_DefaultTrue()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.True(vm.AutoScroll);
});
}
[Fact]
public void AutoScroll_Settable()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.AutoScroll = false;
Assert.False(vm.AutoScroll);
});
}
[Fact]
public void FilterText_DefaultEmpty()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.Equal(string.Empty, vm.FilterText);
});
}
[Fact]
public void FilterText_RaisesPropertyChanged()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
var changed = new List<string>();
vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);
vm.FilterText = "error";
Assert.Contains("FilterText", changed);
Assert.Contains("FilteredLogLines", changed);
});
}
[Fact]
public void ClearCommand_ClearsLogLines()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("test line 1");
vm.LogLines.Add("test line 2");
vm.ClearCommand.Execute(null);
Assert.Empty(vm.LogLines);
});
}
[Fact]
public void FilteredLogLines_NoFilter_ReturnsAll()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] started");
vm.LogLines.Add("[ERROR] failed");
vm.LogLines.Add("[WARN] low memory");
var filtered = vm.FilteredLogLines.ToList();
Assert.Equal(3, filtered.Count);
});
}
[Fact]
public void FilteredLogLines_WithFilter_ReturnsMatches()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] started");
vm.LogLines.Add("[ERROR] something failed");
vm.LogLines.Add("[WARN] low memory");
vm.FilterText = "error";
var filtered = vm.FilteredLogLines.ToList();
Assert.Single(filtered);
Assert.Contains("ERROR", filtered[0]);
});
}
[Fact]
public void FilteredLogLines_CaseInsensitive()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] Test Message");
vm.FilterText = "test message";
Assert.Single(vm.FilteredLogLines);
});
}
[Fact]
public void Commands_AreNotNull()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.NotNull(vm.ClearCommand);
Assert.NotNull(vm.CopyAllCommand);
});
}
}