diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b59e591..29cee641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to Chromatics are documented here. +## 4.3.36 + +- Updated to Sharlayan 9.2.1. +- A device provider that loses one device during start-up no longer reports the whole provider as failed. The skipped device is named and the rest of that provider's devices load as normal. +- The first-run wizard no longer saves a provider as enabled when App Control blocked its library. + +## 4.3.35 + +- Fixed a crash when turning a device provider on or off if Windows App Control had blocked that provider's library. The toggle now returns to its previous position and explains what happened. +- Fixed a crash on the Continue button of the first-run wizard for the same reason. +- Chromatics no longer reports App Control blocks and Hue bridge address problems as application errors, since both are settings on your machine or network rather than faults in Chromatics. + +## 4.3.34 + +- The console tab now shows which version of Chromatics you are running at startup. +- A device provider that fails to start now says so in the console tab, naming the provider and the reason. Providers that fail no longer look like they loaded, and the rest of your devices carry on loading as normal. + +## 4.3.33 + +- Fixed lighting stopping for the rest of the session when the Screen Capture base layer was in use and you returned to the title or character select screen. +- Yeelight, LIFX, Nanoleaf, and Alienware lights that are switched off or off the network at startup now log as device notices instead of errors. + +## 4.3.32 + +- Improved the accuracy and speed of game-data reads on FFXIV patch 7.55, including inventory, chat log, job gauges, and player stats. + ## 4.3.31 - Added support for FFXIV patch 7.55. diff --git a/Chromatics.Tests/Chromatics.Tests.csproj b/Chromatics.Tests/Chromatics.Tests.csproj index 53d68223..8b58cfa7 100644 --- a/Chromatics.Tests/Chromatics.Tests.csproj +++ b/Chromatics.Tests/Chromatics.Tests.csproj @@ -38,7 +38,7 @@ - + diff --git a/Chromatics.Tests/Core/ScreenCaptureProcessorLifetimeTests.cs b/Chromatics.Tests/Core/ScreenCaptureProcessorLifetimeTests.cs new file mode 100644 index 00000000..891ebb6d --- /dev/null +++ b/Chromatics.Tests/Core/ScreenCaptureProcessorLifetimeTests.cs @@ -0,0 +1,21 @@ +using Chromatics.Layers; + +namespace Chromatics.Tests.Core; + +public class ScreenCaptureProcessorLifetimeTests +{ + [Fact] + public void Instance_IsRebuiltAfterDispose() + { + // DisposeAll runs every time the player returns to the title screen and + // nulls the processor's surface. Handing the disposed object back out + // left the Screen Capture base layer attaching to a dead surface on + // every later tick, which killed lighting for the rest of the session. + var first = ScreenCaptureProcessor.Instance; + first.Dispose(); + + var second = ScreenCaptureProcessor.Instance; + + Assert.NotSame(first, second); + } +} diff --git a/Chromatics.Tests/Helpers/AssemblyLoadGuardTests.cs b/Chromatics.Tests/Helpers/AssemblyLoadGuardTests.cs new file mode 100644 index 00000000..c7724666 --- /dev/null +++ b/Chromatics.Tests/Helpers/AssemblyLoadGuardTests.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Reflection; +using Chromatics.Helpers; + +namespace Chromatics.Tests.Helpers; + +public class AssemblyLoadGuardTests +{ + private static Exception AppControlBlock() + => new FileLoadException("An Application Control policy has blocked this file. (0x800711C7)"); + + [Fact] + public void DirectLoadFailure_IsReported() + { + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", AppControlBlock())); + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new FileNotFoundException("missing"))); + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new BadImageFormatException("bad"))); + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new TypeLoadException("type"))); + } + + [Fact] + public void LoadFailureWrappedByAnEventHandler_IsReported() + { + // How it arrives from a routed-event handler (CHROMATICS-1N). + var ex = new TargetInvocationException(AppControlBlock()); + + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Logitech", ex)); + } + + [Fact] + public void LoadFailureInsideAggregate_IsReported() + { + // How it arrives from an unobserved task (CHROMATICS-1G / 1H). + var ex = new AggregateException(new InvalidOperationException("unrelated"), AppControlBlock()); + + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("OpenRGB", ex)); + } + + [Fact] + public void TypeInitializerCarryingALoadFailure_IsReported() + { + var ex = new TypeInitializationException("SomeType", AppControlBlock()); + + Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Corsair", ex)); + } + + [Fact] + public void UnrelatedFailures_AreNotReportedAsLoadFailures() + { + Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue", new InvalidOperationException())); + Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue", new TimeoutException())); + Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue", + new InvalidOperationException("outer", new ArgumentOutOfRangeException()))); + } + + [Fact] + public void TryRun_ReturnsFalseWhenTheActionFaults() + { + Assert.True(AssemblyLoadGuard.TryRun("Razer", () => { })); + Assert.False(AssemblyLoadGuard.TryRun("Razer", () => throw AppControlBlock())); + Assert.False(AssemblyLoadGuard.TryRun("Razer", () => throw new InvalidOperationException())); + } +} diff --git a/Chromatics.Tests/Helpers/NetworkFailureHelperTests.cs b/Chromatics.Tests/Helpers/NetworkFailureHelperTests.cs new file mode 100644 index 00000000..29ca8910 --- /dev/null +++ b/Chromatics.Tests/Helpers/NetworkFailureHelperTests.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using Chromatics.Helpers; + +namespace Chromatics.Tests.Helpers; + +public class NetworkFailureHelperTests +{ + [Fact] + public void ConnectionRefused_IsUnreachable() + { + var ex = new SocketException((int)SocketError.ConnectionRefused); + + Assert.True(NetworkFailureHelper.IsUnreachable(ex)); + } + + [Fact] + public void SocketFailureWrappedInIoException_IsUnreachable() + { + var ex = new IOException("write failed", new SocketException((int)SocketError.HostUnreachable)); + + Assert.True(NetworkFailureHelper.IsUnreachable(ex)); + } + + [Fact] + public void HttpAndTimeoutFailures_AreUnreachable() + { + Assert.True(NetworkFailureHelper.IsUnreachable(new HttpRequestException("no route"))); + Assert.True(NetworkFailureHelper.IsUnreachable(new TimeoutException())); + Assert.True(NetworkFailureHelper.IsUnreachable(new TaskCanceledException())); + } + + [Fact] + public void SocketFailureInsideAggregate_IsUnreachable() + { + var ex = new AggregateException( + new InvalidOperationException("unrelated"), + new SocketException((int)SocketError.TimedOut)); + + Assert.True(NetworkFailureHelper.IsUnreachable(ex)); + } + + [Fact] + public void ApplicationBugs_AreNotUnreachable() + { + Assert.False(NetworkFailureHelper.IsUnreachable(new NullReferenceException())); + Assert.False(NetworkFailureHelper.IsUnreachable(new InvalidOperationException())); + Assert.False(NetworkFailureHelper.IsUnreachable( + new InvalidOperationException("outer", new ArgumentOutOfRangeException()))); + } +} diff --git a/Chromatics.Tests/Helpers/UpdateNotesTests.cs b/Chromatics.Tests/Helpers/UpdateNotesTests.cs index f370b39b..c44e8290 100644 --- a/Chromatics.Tests/Helpers/UpdateNotesTests.cs +++ b/Chromatics.Tests/Helpers/UpdateNotesTests.cs @@ -57,7 +57,7 @@ public void EmptyOrWhitespaceNotes_ReturnEmpty() { Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection("", "4.3.26")); Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(" ", "4.3.26")); - Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null, "4.3.26")); + Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null!, "4.3.26")); } [Fact] diff --git a/Chromatics.Tests/ViewModels/DeviceToggleItemTests.cs b/Chromatics.Tests/ViewModels/DeviceToggleItemTests.cs new file mode 100644 index 00000000..bdd93fe6 --- /dev/null +++ b/Chromatics.Tests/ViewModels/DeviceToggleItemTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Chromatics.ViewModels; + +namespace Chromatics.Tests.ViewModels; + +public class DeviceToggleItemTests +{ + // App Control blocking a vendor DLL threw out of the fire-and-forget task + // the property setter starts, so the finalizer rethrew it as a crash + // (CHROMATICS-1G / 1H). The toggle has to absorb it and show the real state. + private static Exception AppControlBlock() + => new FileLoadException("An Application Control policy has blocked this file. (0x800711C7)"); + + [Fact] + public void EnableFailure_RevertsToOff() + { + var item = new DeviceToggleItem("Razer", "tooltip", false, + () => throw AppControlBlock(), + () => { }); + + item.IsEnabled = true; + + Assert.False(item.IsEnabled); + } + + [Fact] + public void DisableFailure_LeavesToggleOn() + { + var item = new DeviceToggleItem("OpenRGB", "tooltip", true, + () => Task.FromResult(true), + () => throw AppControlBlock()); + + item.IsEnabled = false; + + Assert.True(item.IsEnabled); + } + + [Fact] + public void EnableVeto_LeavesToggleOff() + { + var item = new DeviceToggleItem("Hue", "tooltip", false, + () => Task.FromResult(false), + () => { }); + + item.IsEnabled = true; + + Assert.False(item.IsEnabled); + } + + [Fact] + public void SuccessfulEnable_TurnsToggleOn() + { + var item = new DeviceToggleItem("LIFX", "tooltip", false, + () => Task.FromResult(true), + () => { }); + + item.IsEnabled = true; + + Assert.True(item.IsEnabled); + } +} diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index 7e22007b..c7302bd4 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -10,7 +10,7 @@ net10.0-windows10.0.19041.0 10.0.17763.0 Chromatics.Program - 4.3.31.0 + 4.3.36.0 Danielle Thompson