XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LogiBoardToolBox

【WinUI】罗技鼠标板载驱动程序

公开
关注 0 Fork 0 Star 0
UTF-8
using LogiBoardToolBox.Core.HidPlusPlus;
using LogiBoardToolBox.Core.Lighting;
using LogiBoardToolBox.Core.Macros;
using LogiBoardToolBox.Core.Onboard;
using XFEExtension.NetCore.XUnit.Assertions;
using XFEExtension.NetCore.XUnit.Attributes;

namespace LogiBoardToolBox.Test;

[TestFixture]
public class AdvancedSettingsTests
{
    [Test]
    public async Task LightforceWritesOnlyItsMaskedBitAndSkipsUnchangedState()
    {
        using var mouse = new FeatureMouse(); using var client = new HidPlusPlusClient(mouse, 1);
        var controller = new AdvancedMouseController(client, "G502 X PLUS"); await controller.ReadAsync();
        await controller.SetLightforceAsync(false); Assert.Equal((byte)0xA0, mouse.ModeStatus);
        var write = mouse.Requests.Single(r => r[2] == 11 && r[3] >> 4 == 1);
        Assert.Equal("00A00001", Convert.ToHexString(write, 4, 4));
        await controller.SetLightforceAsync(false); Assert.Equal(1, mouse.Requests.Count(r => r[2] == 11 && r[3] >> 4 == 1));
        await controller.SetLightforceAsync(true); Assert.Equal((byte)0xA1, mouse.ModeStatus);
    }
    [Test]
    public async Task StartupTogglePreservesSixOpaqueBytesAndVerifiesReadback()
    {
        using var mouse = new FeatureMouse(); using var client = new HidPlusPlusClient(mouse, 1);
        var controller = new AdvancedMouseController(client, "G502 X PLUS");
        await controller.SetStartupEffectAsync(false);
        Assert.Equal(false, controller.State!.StartupEnabled!.Value);
        Assert.Equal("0B16212C3742", Convert.ToHexString(mouse.Startup, 4, 6));
        await controller.SetStartupEffectAsync(true); Assert.Equal(true, controller.State!.StartupEnabled!.Value);
        Assert.Equal(false, mouse.Requests.Any(r => r[2] == 12 && r[3] >> 4 is >= 6 and <= 8));
    }
    [Test]
    public async Task AdvertisedHeroWithFirmwareErrorCannotStartOrApplyCalibration()
    {
        using var mouse = new FeatureMouse { HeroUnavailable = true }; using var client = new HidPlusPlusClient(mouse, 1);
        var controller = new AdvancedMouseController(client, "G502 X PLUS"); var state = await controller.ReadAsync();
        Assert.Equal(false, state.CalibrationSupported); Assert.Equal(true, state.CalibrationIssue.Contains("0x05"));
        try { await controller.StartCalibrationAsync(); throw new Exception("Expected rejection"); } catch (NotSupportedException) { }
        try { await controller.SetCalibrationAsync(10); throw new Exception("Expected rejection"); } catch (NotSupportedException) { }
        Assert.Equal(false, mouse.Requests.Any(r => r[2] == 7 && r[3] >> 4 is 9 or 10));
    }
    [Test]
    public async Task HeroCalibrationInfoRequiresSoftwareModeAndBecomesAvailableAfterSwitch()
    {
        using var mouse = new FeatureMouse { Mode = 1 }; using var client = new HidPlusPlusClient(mouse, 1);
        var controller = new AdvancedMouseController(client, "G502 X PLUS"); var board = await controller.ReadAsync();
        Assert.Equal(false, board.CalibrationSupported); Assert.Equal(true, board.CalibrationIssue.Contains("板载模式"));
        Assert.Equal(false, mouse.Requests.Any(r => r[2] == 7 && r[3] >> 4 == 8));
        mouse.Mode = 2; var host = await controller.ReadAsync();
        Assert.Equal(true, host.CalibrationSupported); Assert.Equal((ushort)800, host.Calibration!.DpiX);
    }
    [Test]
    public async Task HitsSensorCannotExposeLightforceAndG502HasNoInventedLodMode()
    {
        using var mouse = new FeatureMouse { Hits = true }; using var client = new HidPlusPlusClient(mouse, 1);
        var controller = new AdvancedMouseController(client, "G502 X PLUS"); var state = await controller.ReadAsync();
        Assert.Equal(false, state.LightforceSupported); Assert.Equal(false, state.SurfaceModeSupported);
        try { await controller.SetLightforceAsync(false); throw new Exception("Expected rejection"); } catch (NotSupportedException) { }
        Assert.Equal(false, mouse.Requests.Any(r => r[2] == 11 && r[3] >> 4 == 1));
    }
    [Test]
    public async Task AutomaticSaveCoalescesAnEditingGesture()
    {
        var count = 0; await using var queue = new ImmediateSaveQueue(() => { count++; return Task.CompletedTask; }, TimeSpan.FromMilliseconds(10));
        for (var i = 0; i < 20; i++) queue.Request();
        await queue.FlushAsync(); Assert.Equal(1, count); Assert.Equal(false, queue.Pending);
    }
    [Test]
    public async Task EditsArrivingDuringFlashAreSerializedAndNotDropped()
    {
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var count = 0; var concurrent = 0; var maximum = 0;
        await using var queue = new ImmediateSaveQueue(async () => { maximum = Math.Max(maximum, ++concurrent); count++; if (count == 1) { entered.SetResult(); await release.Task; } concurrent--; }, TimeSpan.FromMilliseconds(1));
        queue.Request(); await entered.Task; queue.Request(); release.SetResult(); await queue.FlushAsync();
        Assert.Equal(2, count); Assert.Equal(1, maximum);
    }
    [Test]
    public async Task SaveFailureIsSurfacedAndANewEditCanRetry()
    {
        var count = 0; var failures = 0;
        await using var queue = new ImmediateSaveQueue(() => { if (++count == 1) throw new IOException("device disconnected"); return Task.CompletedTask; }, TimeSpan.FromMilliseconds(1));
        queue.Failed += (_, _) => failures++; queue.Request();
        try { await queue.FlushAsync(); throw new Exception("Expected failure"); } catch (IOException) { }
        queue.Request(); await queue.FlushAsync(); Assert.Equal(2, count); Assert.Equal(1, failures);
    }
    [Test]
    public async Task RestoreReturnsNewMacroSpaceToItsExactOriginalErasedBytes()
    {
        var original = Fixture.Create(10); using var mouse = new FakeMouse(original); using var client = new HidPlusPlusClient(mouse, 1);
        var memory = new OnboardMemory(client, original.DeviceIdentity, original.DeviceName); using var scratch = new ScratchDirectory();
        var plan = MemoryWritePlan.Create(original, original.GetProfiles()[0], [new(3, false, MacroCompiler.Compile("mouse 1 down\ndelay 20\nmouse 1 up"))], true);
        await memory.CommitAsync(plan, scratch.Path); var installed = await memory.ReadAsync();
        await memory.CommitAsync(MemoryWritePlan.Restore(installed, original, true), scratch.Path);
        await memory.SetModeAsync(false); var restored = await memory.ReadAsync();
        Assert.Equal(true, original.Sectors.All(p => p.Value.SequenceEqual(restored.Sectors[p.Key]))); Assert.Equal((byte)2, restored.Mode); Assert.Equal((ushort)0, restored.ActiveSector);
    }
    [Test]
    public void AutoSaveReusesExistingShortAndChainedMacros()
    {
        foreach (var script in new[] { "tap A", string.Join('\n', Enumerable.Repeat("tap B", 110)) })
        {
            var snapshot = Fixture.Create(10); var macro = MacroCompiler.Compile(script);
            var first = MemoryWritePlan.Create(snapshot, snapshot.GetProfiles()[0], [new(3, false, macro)], true);
            foreach (var change in first.Changes) snapshot.Sectors[change.Sector] = change.After;
            var next = MemoryWritePlan.Create(snapshot, snapshot.GetProfiles()[0], [new(3, false, macro)], true);
            Assert.Equal(0, next.Changes.Count);
        }
    }
    [Test]
    public void AnimationDirectionsAndHoldCurveProduceDistinctPlayback()
    {
        var red = Enumerable.Repeat(new Rgb(255, 0, 0), 8).ToArray(); var blue = Enumerable.Repeat(new Rgb(0, 0, 255), 8).ToArray();
        var settings = new LightingSettings { Mode = LightsyncMode.Animation, Period = 1000, Frames = [new(0, red), new(500, blue)] };
        Assert.Equal(new Rgb(128, 0, 128), LightingRenderer.Render(settings, 250)[0]);
        Assert.Equal(new Rgb(255, 0, 0), LightingRenderer.Render(settings with { Frames = [new(0, red, true, TransitionCurve.None), new(500, blue)] }, 250)[0]);
        var bounce = settings with { Direction = AnimationDirection.Bounce };
        Assert.Equal(LightingRenderer.Render(bounce, 750)[0], LightingRenderer.Render(bounce, 1250)[0]);
        var reversed = settings with { Direction = AnimationDirection.ReverseCycle };
        Assert.Equal(LightingRenderer.Render(settings, 100)[0], LightingRenderer.Render(reversed, 900)[0]);
        Assert.Equal(true, LightingTransition.Evaluate(TransitionCurve.InCirc, .25) < .25);
        Assert.Equal(true, LightingTransition.Evaluate(TransitionCurve.OutCirc, .25) > .25);
    }
    [Test]
    public void RandomAnimationKeepsEditedFrameDurationsAndReproduciblePreview()
    {
        var red = Enumerable.Repeat(new Rgb(255, 0, 0), 8).ToArray(); var blue = Enumerable.Repeat(new Rgb(0, 0, 255), 8).ToArray();
        var settings = new LightingSettings { Mode = LightsyncMode.Animation, Direction = AnimationDirection.Random, Period = 1000, Frames = [new(0, red, false), new(900, blue, false)] };
        Assert.Equal(red[0], LightingRenderer.Render(settings, 0)[0]); Assert.Equal(red[0], LightingRenderer.Render(settings, 899)[0]);
        Assert.Equal(blue[0], LightingRenderer.Render(settings, 900)[0]); Assert.Equal(blue[0], LightingRenderer.Render(settings, 999)[0]);
        Assert.Equal(red[0], LightingRenderer.Render(settings, 1000)[0]);
        Assert.Equal(LightingRenderer.Render(settings, 12345)[0], LightingRenderer.Render(settings, 12345)[0]);
    }
}