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

XFESystem

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

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

namespace XFESystem.Desktop.Tests;

/// <summary>Shell input, storage and ANSI output checks without a native VM.</summary>
public sealed class TerminalShellTests
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void CopyPreservesInvalidUtf8AndBomBytesOnRealFileStyleStorage(bool bom)
    {
        var storage=new FileStyleStorage();var shell=new TerminalShell(storage);
        byte[] bytes=bom?[0xef,0xbb,0xbf,65,13,10]:[0x9f,0xff,0,1,2];
        storage.WriteBytes(shell.ResolvePath("Source.bin"),bytes);
        Assert.Equal(0,Run(shell,"cp Source.bin Copy.bin").ExitCode);
        Assert.Equal(bytes,storage.ReadBytes(shell.ResolvePath("Copy.bin")));
        Assert.Equal(1,Run(shell,"cp Source.bin Copy.bin").ExitCode);
        Assert.Equal(bytes,storage.ReadBytes(shell.ResolvePath("Copy.bin")));
    }
    sealed class FileStyleStorage : IWorkspaceBinaryStorage
    {
        readonly MemoryWorkspace inner=new(false);
        public bool IsPersistent=>false;
        public bool Exists(string path)=>inner.Exists(path);
        public WorkspaceEntry[] List(string path)=>inner.List(path);
        public byte[] ReadBytes(string path)=>inner.ReadBytes(path);
        public string ReadText(string path)
        {using var reader=new StreamReader(new MemoryStream(ReadBytes(path)),new System.Text.UTF8Encoding(false,true),true);return reader.ReadToEnd();}
        public void WriteText(string path,string text)=>inner.WriteText(path,text);
        public void WriteBytes(string path,ReadOnlySpan<byte> bytes)=>inner.WriteBytes(path,bytes);
        public void CreateDirectory(string path)=>inner.CreateDirectory(path);
        public void Move(string source,string destination)=>inner.Move(source,destination);
        public void Delete(string path)=>inner.Delete(path);
    }
    [Fact]
    public void StartupOnlyShowsIdentityVersionAndHelpAndKeepsBlueAnsiTheme()
    {
        var shell = new TerminalShell(new MemoryWorkspace(false), "TestVersion");
        string text = Screen(shell);
        Assert.Contains("XFESystem|XFEstudio", text); Assert.Contains("TestVersion", text); Assert.Contains("help", text);
        Assert.DoesNotContain("Run Demo", text); Assert.DoesNotContain("sample", text);
        Assert.Equal(TerminalColor.ThemeForeground, shell.ReadOutput().GetCell(0, 0).Style.Foreground);
        Assert.Equal("#75C9F5", shell.ReadOutput().GetCell(0, 0).Style.Foreground.Hex);
        Assert.Equal(SystemPaths.Documents + "> ", shell.Prompt);
    }

    [Fact]
    public void QuotedDirectoriesAndFilesUseRealRamStorageAndMFPreservesExistingData()
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
        Assert.Equal(0, Run(shell, "md \"Folder One\"").ExitCode);
        Assert.Equal(0, Run(shell, "cd \"Folder One\"").ExitCode);
        Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
        Assert.Equal(0, Run(shell, "write \"hello world.txt\" \"Hello 世界\"").ExitCode);
        Assert.Equal(0, Run(shell, "append \"hello world.txt\" \" + XFE\"").ExitCode);
        Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
        string file = SystemPaths.Documents + "\\Folder One\\hello world.txt";
        Assert.Equal("Hello 世界 + XFE", storage.ReadText(file));
        Assert.Equal(0, Run(shell, "cat \"hello world.txt\"").ExitCode);
        Assert.Contains("Hello 世界 + XFE", Screen(shell));
        Assert.Equal(0, Run(shell, "file \"hello world.txt\"").ExitCode);
        Assert.Contains("hello world.txt", Screen(shell));
        Run(shell, "cp \"hello world.txt\" Copy.txt"); Run(shell, "mv Copy.txt Moved.txt");
        Assert.Equal("Hello 世界 + XFE", storage.ReadText(shell.ResolvePath("Moved.txt")));
        Run(shell, "del Moved.txt"); Assert.False(storage.Exists(shell.ResolvePath("Moved.txt")));
        Assert.Equal(0, Run(shell, "cd ..").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
    }

    [Fact]
    public void FailedCommandsDoNotChangeDirectoryOrOverwriteCopyDestinations()
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
        Run(shell, "write First.txt original"); Run(shell, "write Second.txt retained");
        Assert.Equal(1, Run(shell, "cp First.txt Second.txt").ExitCode);
        Assert.Equal("retained", storage.ReadText(shell.ResolvePath("Second.txt")));
        Assert.Equal(1, Run(shell, "cd First.txt").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
        Assert.Equal(1, Run(shell, "cd Missing").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
        Assert.Equal(1, Run(shell, "mf \"missing quote").ExitCode);
        Assert.Equal(127, Run(shell, "unknowncommand").ExitCode);
        Assert.Equal(0, Run(shell, "help").ExitCode); Assert.Contains("RAM session", Screen(shell));
    }

    [Fact]
    public void HistoryCanVisitAllEntriesAndRestoreTheDraftWithoutExposingMutableStorage()
    {
        var shell = new TerminalShell(new MemoryWorkspace(false));
        Run(shell, "echo first"); Run(shell, "echo second"); Run(shell, "echo third");
        shell.InsertText("draft"); shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo third", shell.Input);
        shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo second", shell.Input);
        shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
        shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
        for (int index = 0; index < 3; index++) shell.HandleKey(TerminalShellKey.Down);
        Assert.Equal("draft", shell.Input); Assert.Equal(5, shell.Caret);
    }

    [Fact]
    public void CursorInsertionOverwriteAndDeletionPreserveUnicodeScalars()
    {
        var shell = new TerminalShell(new MemoryWorkspace(false));
        shell.InsertText("a😀c"); shell.HandleKey(TerminalShellKey.Left); Assert.Equal(3, shell.Caret);
        shell.HandleKey(TerminalShellKey.Left); Assert.Equal(1, shell.Caret);
        shell.HandleKey(TerminalShellKey.Delete); Assert.Equal("ac", shell.Input);
        shell.InsertText("b"); Assert.Equal("abc", shell.Input);
        shell.HandleKey(TerminalShellKey.Home); shell.HandleKey(TerminalShellKey.Insert); shell.InsertText("😀");
        Assert.Equal("😀bc", shell.Input); Assert.Equal(2, shell.Caret);
        shell.HandleKey(TerminalShellKey.Backspace); Assert.Equal("bc", shell.Input); Assert.Equal(0, shell.Caret);
        shell.HandleKey(TerminalShellKey.End); shell.HandleKey(TerminalShellKey.Right); Assert.Equal(2, shell.Caret);
    }

    [Fact]
    public void CancelAndPasteCannotExecuteCommandsOrInjectAnsiControls()
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
        shell.InsertText("mf MustNotExist.txt"); shell.HandleKey(TerminalShellKey.Cancel);
        Assert.False(storage.Exists(shell.ResolvePath("MustNotExist.txt"))); Assert.Empty(shell.Input); Assert.Empty(shell.History);
        Assert.False(shell.InsertText("mf Injected.txt\n")); Assert.Empty(shell.Input);
        Assert.False(shell.InsertText("\u001b[2J")); Assert.False(shell.InsertText("\ud800"));
        Assert.Contains("^C", Screen(shell));
    }

    [Fact]
    public void InputHistoryAndOutputRemainBounded()
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
        Assert.True(shell.InsertText(new string('x', TerminalShell.MaximumInputCharacters)));
        Assert.False(shell.InsertText("y")); Assert.Equal(TerminalShell.MaximumInputCharacters, shell.Input.Length);
        shell.HandleKey(TerminalShellKey.Cancel);
        for (int index = 0; index < 80; index++) Run(shell, "echo " + index);
        Assert.Equal(TerminalShell.MaximumHistoryEntries, shell.History.Count); Assert.Equal("echo 16", shell.History[0]);
        storage.WriteText(shell.ResolvePath("Large.txt"), new string('A', 20_000)); Run(shell, "cat Large.txt");
        Assert.Contains("Output truncated", Screen(shell));
    }

    [Fact]
    public void ApplicationAnsiOutputUsesTheOriginalCellModelAndClsResetsIt()
    {
        var shell = new TerminalShell(new MemoryWorkspace(false)); Run(shell, "cls");
        shell.ProgramOutput.Write("\u001b[38;2;23;45;67mX\u001b[0mY");
        var frame = shell.ReadOutput(); Assert.Equal("X", frame.GetCell(0, 0).Text);
        Assert.Equal(new TerminalColor(23, 45, 67), frame.GetCell(0, 0).Style.Foreground);
        Assert.Equal(TerminalColor.ThemeForeground, frame.GetCell(1, 0).Style.Foreground);
        Run(shell, "cls"); Assert.Equal(0, shell.ReadOutput().CursorRow); Assert.Equal(0, shell.ReadOutput().CursorColumn);
    }

    [Fact]
    public void OpenRunAndExitReturnRequestsWithoutExecutingUiOrProgramsOnTheInputThread()
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage); Run(shell, "mf Example.cs");
        Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "open Example.cs").OpenPath);
        Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "run Example.cs").ManagedProgramPath);
        Assert.True(Run(shell, "exit").CloseRequested);
        Assert.Equal(1, Run(shell, "run Missing.dll").ExitCode);
    }

    [Fact]
    public void ParserPreservesWindowsBackslashesEmptyArgumentsAndRejectsUnclosedQuotes()
    {
        Assert.Equal(new[] { "write", @"X:\Folder One\File.txt", "" }, TerminalShell.ParseCommandLine("write \"X:\\Folder One\\File.txt\" \"\""));
        Assert.Equal(new[] { "echo", "Hello 世界" }, TerminalShell.ParseCommandLine("echo 'Hello 世界'"));
        Assert.Throws<ArgumentException>(() => TerminalShell.ParseCommandLine("echo 'broken"));
        var shell = new TerminalShell(new MemoryWorkspace(false));
        Assert.Equal(SystemPaths.Root + "File.txt", shell.ResolvePath("/../../File.txt"));
        Assert.Throws<ArgumentException>(() => shell.ResolvePath(@"C:\HostSecret.txt"));
    }

    [Fact]
    public void NativeLfHostKeepsEveryEchoAndReadOutputAtColumnZeroIncludingPartialProgramLines()
    {
        var shell = new TerminalShell(new MemoryWorkspace(false), columns: 96, rows: 128);
        shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
        shell.ProgramOutput.Write("progress\r");
        string[] commands = ["help", "md CliProof", "mf CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady",
            "cat CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady", "echo draft", "echo abd", "history"];
        foreach (string command in commands)
        {
            int before = shell.ReadOutput().CursorRow;
            Assert.Equal(0, Run(shell, command).ExitCode);
            var frame = shell.ReadOutput(); Assert.Equal(0, frame.CursorColumn);
            var fresh = Enumerable.Range(before, frame.CursorRow - before + 1).Select(row => Row(frame, row)).ToArray();
            Assert.Contains(shell.Prompt + command, fresh);
            if (command.StartsWith("cat", StringComparison.Ordinal)) Assert.Contains("NativeTerminalReady", fresh);
            if (command == "echo draft") Assert.Contains("draft", fresh);
            if (command == "echo abd") Assert.Contains("abd", fresh);
        }
        Assert.Equal("progress", Row(shell.ReadOutput(), 0));
    }

    [Theory]
    [InlineData("\n")]
    [InlineData("\r\n")]
    public void CatDisplaysLogicalFileLinesAtColumnZeroWithoutChangingStoredBytes(string ending)
    {
        var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
        shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
        string path = shell.ResolvePath("Lines.txt"), source = "first" + ending + "second";
        storage.WriteText(path, source); Assert.Equal(0, Run(shell, "cat Lines.txt").ExitCode);
        var frame = shell.ReadOutput();
        Assert.Equal(shell.Prompt + "cat Lines.txt", Row(frame, 0));
        Assert.Equal("first", Row(frame, 1)); Assert.Equal("second", Row(frame, 2));
        Assert.Equal(0, frame.CursorColumn); Assert.Equal(source, storage.ReadText(path));
    }

    private static string Row(TerminalSnapshot frame, int row) => string.Concat(Enumerable.Range(0, frame.Columns)
        .Where(column => !frame.GetCell(column, row).IsContinuation).Select(column => frame.GetCell(column, row).Text)).TrimEnd();

    private static TerminalShellCommandResult Run(TerminalShell shell, string command)
    { Assert.True(shell.InsertText(command)); return shell.Submit(); }
    private static string Screen(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text));
}