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

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using XFESystem.Core;
using XFESystem.Desktop;
using XFESystem.Kernel;
using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

namespace XFESystem.Desktop.Platform.Tests;

public sealed class NativeInputTests
{
    [Theory]
    [InlineData((byte)0x4F,"End")]
    [InlineData((byte)0xCF,"End")]
    [InlineData((byte)0x47,"Home")]
    [InlineData((byte)0xC7,"Home")]
    public void PinnedUsNavigationCodesUseDeclaredEndAndHomeMappings(byte scan,string expected)
    {
        // Cosmos publishes its pure managed layout implementation only under
        // linux-x64. Load that exact pinned binary without loading guest-only
        // hardware managers into the Windows test runner.
        string packages=Environment.GetEnvironmentVariable("NUGET_PACKAGES")??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),".nuget","packages");
        string path=Path.Combine(packages,"cosmos.kernel.system","3.0.94","runtimes","linux-x64","lib","net10.0","Cosmos.Kernel.System.dll");
        var assembly=System.Reflection.Assembly.LoadFrom(path);
        var layout=Activator.CreateInstance(assembly.GetType("Cosmos.Kernel.System.Input.Layouts.USStandardLayout",true)!)!;
        var convert=assembly.GetType("Cosmos.Kernel.System.Input.KeyboardLayout",true)!.GetMethod("ConvertScanCode",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic)!;
        var key=convert.Invoke(layout,[scan,false,false,false,false,false]);Assert.NotNull(key);
        Assert.Equal(expected,key.GetType().GetProperty("Key")!.GetValue(key)!.ToString());
        Assert.Equal('\0',key.GetType().GetProperty("KeyChar")!.GetValue(key));
    }
    private sealed class Canvas : IDesktopCanvas
    {
        public int Width => 1280;
        public int Height => 800;
        public void Fill(uint rgb, int x, int y, int width, int height) { }
        public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
        public void Text(string text, uint rgb, int x, int y) { }
        public void Present() { }
    }
    private static DesktopSession Session() => new(new Canvas(), new MemoryWorkspace());
    private static DesktopKey? Translate(NativeDesktopInput input, DesktopSession session, byte code, bool up = false, bool lost = false)
    {
        var key = input.Translate(code, up, lost, session, out var character);
        Assert.Equal('\0', character);
        return key;
    }
    private static void Route(NativeDesktopInput input, DesktopSession session, byte code, bool up = false, bool lost = false)
    {
        var key = input.Translate(code, up, lost, session, out var character);
        if (key is { } command) session.Key(command, character);
    }
    private static void Drain()
    {
        while (NativeKeyboardCapture.TryRead(out _, out _, out _)) { }
    }

    [Theory]
    [InlineData((byte)0x5B)]
    [InlineData((byte)0x5C)]
    public void BareWindowsKeyOpensStartExactlyOnceAtRelease(byte windows)
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        Assert.Null(Translate(input, desktop, windows));
        Assert.Null(Translate(input, desktop, windows));
        Assert.Equal(DesktopKey.StartMenu, Translate(input, desktop, windows, up: true));
        Assert.Null(Translate(input, desktop, windows, up: true));
    }

    [Fact]
    public void IsolatedWindowsReleaseDoesNotOpenStartMenu()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        Assert.Null(Translate(input, desktop, 0x5B, up: true));
        Assert.Null(Translate(input, desktop, 0x5C, up: true));
    }

    [Fact]
    public void WinEOpensFilesWithoutLeakingCharacterOrStartMenu()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        var editor = desktop.Open(DesktopApplication.Editor);
        Route(input, desktop, 0x5B); Route(input, desktop, 0x12);
        Route(input, desktop, 0x12, up: true); Route(input, desktop, 0x5B, up: true);
        Assert.Equal(DesktopApplication.Files, desktop.ActiveWindow!.Application);
        Assert.Empty(editor.Content); Assert.False(desktop.IsStartMenuOpen);
        Route(input, desktop, 0x5B); Route(input, desktop, 0x5B, up: true);
        Assert.True(desktop.IsStartMenuOpen);
    }

    [Theory]
    [InlineData((byte)0x20, DesktopKey.ShowDesktop)]
    [InlineData((byte)0x4B, DesktopKey.SnapLeft)]
    [InlineData((byte)0x4D, DesktopKey.SnapRight)]
    [InlineData((byte)0x48, DesktopKey.Maximize)]
    [InlineData((byte)0x50, DesktopKey.Restore)]
    public void WindowsChordRoutesSpecificDesktopCommand(byte scan, DesktopKey expected)
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        Translate(input, desktop, 0x5B);
        Assert.Equal(expected, Translate(input, desktop, scan));
        Assert.Null(Translate(input, desktop, scan, up: true));
        Assert.Null(Translate(input, desktop, 0x5B, up: true));
    }

    [Fact]
    public void BothWindowsKeysReleaseProducesOneStartCommand()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        Assert.Null(Translate(input, desktop, 0x5B)); Assert.Null(Translate(input, desktop, 0x5C));
        Assert.Null(Translate(input, desktop, 0x5B, up: true));
        Assert.Equal(DesktopKey.StartMenu, Translate(input, desktop, 0x5C, up: true));
    }

    [Fact]
    public void WindowsModifierChordDoesNotTriggerBareWindowsMenu()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        Translate(input, desktop, 0x5B); Translate(input, desktop, 0x2A);
        Assert.Null(Translate(input, desktop, 0x5B, up: true));
        Translate(input, desktop, 0x2A, up: true);
    }

    [Fact]
    public void AltTabRemainsOneStableCycleUntilAltRelease()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        var first = desktop.Open(DesktopApplication.Files); var second = desktop.Open(DesktopApplication.Editor);
        var third = desktop.Open(DesktopApplication.Terminal);
        Route(input, desktop, 0x38);
        Route(input, desktop, 0x0F); Assert.Same(second, desktop.ActiveWindow);
        Route(input, desktop, 0x0F, up: true); Assert.True(desktop.IsWindowSwitching);
        Route(input, desktop, 0x0F); Assert.Same(first, desktop.ActiveWindow);
        Route(input, desktop, 0x0F, up: true); Route(input, desktop, 0x0F); Assert.Same(third, desktop.ActiveWindow);
        Route(input, desktop, 0x0F, up: true); Route(input, desktop, 0x38, up: true);
        Assert.False(desktop.IsWindowSwitching); Assert.Empty(second.Content);
    }

    [Fact]
    public void ShiftAltTabReversesAndWaitsForBothAltReleases()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        desktop.Open(DesktopApplication.Files); var second = desktop.Open(DesktopApplication.Editor);
        desktop.Open(DesktopApplication.Terminal);
        Route(input, desktop, 0x38); Route(input, desktop, 0x60); Route(input, desktop, 0x2A);
        Assert.Equal(DesktopKey.SwitchWindowBack, Translate(input, desktop, 0x0F));
        Route(input, desktop, 0x0F); Route(input, desktop, 0x38, up: true);
        Assert.True(desktop.IsWindowSwitching);
        Route(input, desktop, 0x60, up: true); Assert.False(desktop.IsWindowSwitching);
        Assert.Empty(second.Content);
    }

    [Fact]
    public void CtrlSSavesGuestEditorWithoutTypingS()
    {
        var storage = new MemoryWorkspace(); var desktop = new DesktopSession(new Canvas(), storage);
        var editor = desktop.Open(DesktopApplication.Editor); desktop.Key(DesktopKey.Character, 'X');
        var input = new NativeDesktopInput(); Route(input, desktop, 0x1D); Route(input, desktop, 0x1F);
        Route(input, desktop, 0x1F, up: true); Route(input, desktop, 0x1D, up: true);
        Assert.False(editor.IsDirty); Assert.Equal("X", storage.ReadText(editor.Location));
    }

    [Fact]
    public void CtrlSInFocusedWebEditorReachesDesktopSaveInsteadOfBrowserKeyHandler()
    {
        var storage=new MemoryWorkspace();var browser=new FocusedWebEditor();
        var desktop=new DesktopSession(new Canvas(),storage,browser);
        var editor=desktop.Open(DesktopApplication.Editor,SystemPaths.Combine(SystemPaths.Documents,"Hello.cs"));
        browser.Model="public class SavedByDesktopShortcut { }";
        Assert.True(desktop.HasBrowserContentFocus);
        var input=new NativeDesktopInput();
        Route(input,desktop,0x1D);Route(input,desktop,0x1F);
        Route(input,desktop,0x1F,up:true);Route(input,desktop,0x1D,up:true);
        Assert.Equal(browser.Model,storage.ReadText(editor.Location));
        Assert.DoesNotContain(browser.Keys,key=>key.Key.Equals("s",StringComparison.OrdinalIgnoreCase));
        Assert.Contains("Saved to RAM live",editor.Status,StringComparison.Ordinal);
    }

    private sealed class FocusedWebEditor : IBrowserService,IInteractiveBrowserFactory,IInteractiveBrowserSession
    {
        public string Model="";
        public List<BrowserKeyboardInput> Keys=[];
        public BrowserPage CurrentPage {get;private set;}=new("about:Start","Test","");
        public long Revision=>0;
        public double DocumentHeight=>0;
        public IInteractiveBrowserSession CreatePageSession()=>this;
        public BrowserPage Navigate(string address)=>CurrentPage=new(address,"Test","");
        public BrowserPage NavigateEditor(string source,string path){Model=source;return CurrentPage=new(path,"Test",source);}
        public string? ReadEditorText()=>Model;
        public string ExecuteJavaScript(string source)=>"";
        public string PreviewMarkdown(string markdown)=>markdown;
        public void Paint(IDesktopCanvas canvas,DesktopBounds viewport,double scrollY){}
        public bool Pointer(BrowserPointerInput input)=>false;
        public bool Key(BrowserKeyboardInput input){Keys.Add(input);return false;}
        public bool AdvanceTime(long monotonicMilliseconds)=>false;
    }

    [Fact]
    public void AltF4PreservesUnsavedCloseGuard()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        var editor = desktop.Open(DesktopApplication.Editor); desktop.Key(DesktopKey.Character, 'X');
        Route(input, desktop, 0x38); Route(input, desktop, 0x3E); Route(input, desktop, 0x38, up: true);
        Assert.Single(desktop.Windows); Assert.True(editor.IsDirty); Assert.Contains("Unsaved", editor.Status);
    }

    [Fact]
    public void LossResetsModifiersEndsCycleAndRejectsStaleWindowsRelease()
    {
        var input = new NativeDesktopInput(); var desktop = Session();
        desktop.Open(DesktopApplication.Files); desktop.Open(DesktopApplication.Editor);
        Route(input, desktop, 0x38); Route(input, desktop, 0x0F); Route(input, desktop, 0x5B);
        Assert.True(desktop.IsWindowSwitching);
        Route(input, desktop, 0, up: true, lost: true);
        Assert.False(desktop.IsWindowSwitching);
        Assert.Null(Translate(input, desktop, 0x5B, up: true));
        Route(input, desktop, 0x5B); Route(input, desktop, 0x5B, up: true); Assert.True(desktop.IsStartMenuOpen);
    }

    [Fact]
    public void CapturePreservesOrderedMakeAndBreakReports()
    {
        Drain();
        NativeKeyboardCapture.Observe(0x5B, false); NativeKeyboardCapture.Observe(0x12, false);
        NativeKeyboardCapture.Observe(0x12, true); NativeKeyboardCapture.Observe(0x5B, true);
        (byte Scan, bool Up)[] expected = [(0x5B, false), (0x12, false), (0x12, true), (0x5B, true)];
        foreach (var item in expected)
        {
            Assert.True(NativeKeyboardCapture.TryRead(out var scan, out var up, out var lost));
            Assert.Equal(item.Scan, scan); Assert.Equal(item.Up, up); Assert.False(lost);
        }
        Assert.False(NativeKeyboardCapture.TryRead(out _, out _, out _));
    }

    [Fact]
    public void OverflowDiscardsOldModifierReportsAndSignalsOneReset()
    {
        Drain(); NativeKeyboardCapture.Observe(0x5B, false);
        for (int i = 0; i < 600; i++) NativeKeyboardCapture.Observe(0x12, false);
        NativeKeyboardCapture.Observe(0x5B, true);
        Assert.True(NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost));
        Assert.Equal(0, scan); Assert.True(released); Assert.True(lost);
        Assert.False(NativeKeyboardCapture.TryRead(out _, out _, out _));
        NativeKeyboardCapture.Observe(0x5B, false); NativeKeyboardCapture.Observe(0x5B, true);
        Assert.True(NativeKeyboardCapture.TryRead(out scan, out released, out lost));
        Assert.Equal(0x5B, scan); Assert.False(released); Assert.False(lost);
        Assert.True(NativeKeyboardCapture.TryRead(out scan, out released, out lost)); Assert.True(released);
    }

    [Fact]
    public async Task ConcurrentWritersNeverPairScancodeWithAnotherWritersReleaseFlag()
    {
        Drain();
        var writers = Enumerable.Range(1, 4).Select(index => Task.Run(() =>
        {
            for (int i = 0; i < 10000; i++) NativeKeyboardCapture.Observe((byte)index, (index & 1) != 0);
        })).ToArray();
        var completed = Task.WhenAll(writers);
        while (!completed.IsCompleted)
        {
            while (NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost))
            {
                if (lost) { Assert.Equal(0, scan); Assert.True(released); continue; }
                Assert.InRange(scan, 1, 4); Assert.Equal((scan & 1) != 0, released);
            }
            await Task.Yield();
        }
        await completed;
        while (NativeKeyboardCapture.TryRead(out var scan, out var released, out var lost))
            if (!lost) { Assert.InRange(scan, 1, 4); Assert.Equal((scan & 1) != 0, released); }
        NativeKeyboardCapture.Observe(0x7E, false);
        Assert.True(NativeKeyboardCapture.TryRead(out var marker, out var markerReleased, out var markerLost));
        Assert.Equal(0x7E, marker); Assert.False(markerReleased); Assert.False(markerLost);
    }
}