using XFESystem.Core; using XFESystem.Desktop; using XFESystem.NativeKernel; using XFESystem.NativeRuntime.ConsoleApplication; namespace XFESystem.ConsoleEdition.Tests; public sealed class ConsoleEditionTests { [Fact] public unsafe void IncompleteFramesCannotReleaseBootAndCompleteFramesReleaseItOnce() { uint[] scanout = new uint[320*240]; int handoffs = 0; fixed(uint* pointer=scanout) { var framebuffer = new ConsoleMemoryFramebuffer(new((nuint)pointer,(nuint)(scanout.Length*4),320,240,1280,32,0xFF0000,0xFF00,0xFF), () => { Assert.All(scanout,p=>Assert.Equal(0u,p)); handoffs++; }); Assert.Throws(()=>framebuffer.Present(ReadOnlySpan.Empty)); Assert.Equal(0,handoffs); int[] complete = Enumerable.Repeat(0x123456,320*240).ToArray(); framebuffer.Present(complete); Assert.Equal(1,handoffs); Assert.Equal(0x123456u,scanout[0]); framebuffer.Present(complete); Assert.Equal(1,handoffs); } } private sealed class Scanout(int width, int height) : IConsoleFramebuffer { public int Width => width; public int Height => height; public int[] Pixels = new int[checked(width * height)]; public int Presentations; public void Present(ReadOnlySpan pixels) { pixels.CopyTo(Pixels); Presentations++; } } [Theory] [InlineData(1024, 768, 100, 12, 20, 81, 31)] [InlineData(3840, 2160, 200, 24, 40, 156, 46)] [InlineData(320, 240, 100, 12, 20, 24, 4)] public void ClassicLayoutPreservesRustCellAndHeaderGeometry(int width, int height, int scale, int cellWidth, int cellHeight, int columns, int rows) { var scanout = new Scanout(width, height); var renderer = new ClassicConsoleRenderer(scanout); Assert.Equal(scale, renderer.ScalePercent); Assert.Equal(cellWidth, renderer.CellWidth); Assert.Equal(cellHeight, renderer.CellHeight); Assert.Equal(columns, renderer.Columns); Assert.Equal(rows, renderer.Rows); var shell = new TerminalShell(new MemoryWorkspace(false), "0.3.0-dev.4", columns, 128); renderer.Render(shell); Assert.Equal(1, scanout.Presentations); Assert.Equal(1, renderer.FramesPresented); Assert.Equal((int)ClassicConsoleRenderer.Accent, scanout.Pixels[0]); Assert.Equal((int)ClassicConsoleRenderer.Panel, scanout.Pixels[(height - 1) * width]); Assert.Equal((int)ClassicConsoleRenderer.Background, scanout.Pixels[(renderer.BodyTop + 2) * width]); } [Fact] public void ConsoleAssemblyHasNoGraphicalOrNetworkDependencies() { var names = typeof(ClassicConsoleRenderer).Assembly.GetReferencedAssemblies().Select(name => name.Name).ToArray(); foreach (string excluded in new[] { "XFESystem.Desktop", "XFESystem.Browser", "XFESystem.Platform", "XFESystem.Documents", "XFESystem.Tls", "XFESystem.Network" }) Assert.DoesNotContain(excluded, names); Assert.Contains("XFESystem.Console", names); Assert.Contains("XFESystem.ManagedRuntime", names); } [Fact] public void ShellTypingAndCaretAreRenderedWithoutMutatingCompletedOutput() { var scanout = new Scanout(1024, 768); var renderer = new ClassicConsoleRenderer(scanout); var shell = new TerminalShell(new MemoryWorkspace(false), columns: renderer.Columns, rows: 128); renderer.Render(shell); int[] initial = (int[])scanout.Pixels.Clone(); string output = Text(shell); shell.InsertText("echo typed"); shell.HandleKey(TerminalShellKey.Left); renderer.Render(shell); Assert.Equal(output, Text(shell)); Assert.False(initial.SequenceEqual(scanout.Pixels)); int[] edited = (int[])scanout.Pixels.Clone(); renderer.Render(shell, cursorVisible: false); Assert.False(edited.SequenceEqual(scanout.Pixels)); shell.HandleKey(TerminalShellKey.Enter); renderer.Render(shell); Assert.Contains("typed", Text(shell)); Assert.Equal("", shell.Input); } [Fact] public void UnicodeAndAnsiProduceActualClassicFramebufferPixels() { var scanout = new Scanout(1024, 768); var renderer = new ClassicConsoleRenderer(scanout); var shell = new TerminalShell(new MemoryWorkspace(false), columns: renderer.Columns, rows: 128); shell.ProgramOutput.Write("\u001bc\u001b[38;2;255;80;40m中文\u001b[0m\r\n"); renderer.Render(shell); Assert.Contains(0xFF5028, scanout.Pixels.Skip(renderer.BodyTop * scanout.Width).Take(renderer.CellHeight * scanout.Width)); Assert.Contains((int)ClassicConsoleRenderer.Foreground, scanout.Pixels.Skip((renderer.BodyTop + renderer.CellHeight) * scanout.Width)); } [Fact] public void LongInputWrapsOnSmallScreenAndKeepsCaretVisible() { var scanout = new Scanout(320, 240); var renderer = new ClassicConsoleRenderer(scanout); var shell = new TerminalShell(new MemoryWorkspace(false), columns: renderer.Columns, rows: 128); shell.InsertText(new string('x', 1024)); renderer.Render(shell); shell.MoveCaret(0); renderer.Render(shell); Assert.Equal(2, scanout.Presentations); int bottom = renderer.BodyTop + renderer.Rows * renderer.CellHeight; Assert.Contains((int)ClassicConsoleRenderer.Accent, scanout.Pixels.Skip(renderer.BodyTop * 320).Take((bottom - renderer.BodyTop) * 320)); } [Fact] public void ScrollbackShowsOlderOutputAndLeavesPromptUsable() { var scanout = new Scanout(640, 480); var renderer = new ClassicConsoleRenderer(scanout); var shell = new TerminalShell(new MemoryWorkspace(false), columns: renderer.Columns, rows: 128); for (int index = 0; index < 80; index++) shell.ProgramOutput.WriteLine("line " + index); renderer.Render(shell); int[] latest = (int[])scanout.Pixels.Clone(); renderer.Render(shell, scrollRows: 30); Assert.False(latest.SequenceEqual(scanout.Pixels)); shell.InsertText("echo back"); shell.HandleKey(TerminalShellKey.Enter); renderer.Render(shell); Assert.Contains("back", Text(shell)); Assert.Equal(3, scanout.Presentations); } [Theory] [InlineData(0x1C, TerminalShellKey.Enter)] [InlineData(0x14B, TerminalShellKey.Left)] [InlineData(0x147, TerminalShellKey.Home)] [InlineData(0x153, TerminalShellKey.Delete)] [InlineData(0x152, TerminalShellKey.Insert)] public void ConsoleKeyboardUsesRealShellNavigation(uint code, TerminalShellKey expected) { Assert.Equal(expected, ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyDown, KeyCode = code })!.Value.Key); Assert.Null(ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyUp, KeyCode = code })); } [Fact] public void ModifierKeysCannotOpenGuiOrInjectControlCharacters() { var typed = ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyDown, KeyCode = 0x1E, Unicode = 'A' }); Assert.Equal("A", typed!.Value.Text); Assert.Null(ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyDown, KeyCode = 0x12, Unicode = 'e', Modifiers = Ps2Decoder.Super })); Assert.Null(ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyDown, KeyCode = 0x3E, Modifiers = Ps2Decoder.Alt })); Assert.Equal(TerminalShellKey.Cancel, ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.KeyDown, KeyCode = 0x2E, Modifiers = Ps2Decoder.Control })!.Value.Key); Assert.Null(ConsoleKeyboard.Translate(new() { Kind = Ps2Decoder.Pointer, Unicode = 'x' })); } [Theory] [InlineData(16, 0xF800u, 0x7E0u, 0x1Fu)] [InlineData(24, 0xFFu, 0xFF00u, 0xFF0000u)] [InlineData(32, 0xFF0000u, 0xFF00u, 0xFFu)] public unsafe void RawScanoutHonorsPitchAndChannelMasksWithoutTouchingGuards(int bits, uint red, uint green, uint blue) { const int width = 320, height = 240, guard = 64; int bytes = bits / 8, pitch = width * bytes + 16; byte[] memory = Enumerable.Repeat((byte)0xA5, guard + pitch * height + guard).ToArray(); fixed (byte* pointer = memory) { var presenter = new ConsoleMemoryFramebuffer(new((nuint)(pointer + guard), (nuint)(pitch * height), width, height, pitch, bits, red, green, blue)); int[] pixels = new int[width * height]; pixels.AsSpan().Fill(0xFF0000); presenter.Present(pixels); } Assert.All(memory.Take(guard), value => Assert.Equal(0xA5, value)); Assert.All(memory.Skip(guard + pitch * height), value => Assert.Equal(0xA5, value)); for (int row = 0; row < height; row++) Assert.All(memory.Skip(guard + row * pitch + width * bytes).Take(16), value => Assert.Equal(0xA5, value)); uint packed = 0; for (int index = 0; index < bytes; index++) packed |= (uint)memory[guard + index] << (index * 8); Assert.Equal(red, packed); } [Fact] public unsafe void FramebufferRejectsAliasedMasksShortMappingAndIncompleteFrames() { byte[] memory = new byte[320 * 240 * 4]; fixed (byte* pointer = memory) { nuint address = (nuint)pointer; Assert.Throws(() => new ConsoleMemoryFramebuffer(new(address, (nuint)memory.Length, 320, 240, 1280, 32, 0xFF00, 0xFF00, 0xFF))); Assert.Throws(() => new ConsoleMemoryFramebuffer(new(address, 10, 320, 240, 1280, 32, 0xFF0000, 0xFF00, 0xFF))); var presenter = new ConsoleMemoryFramebuffer(new(address, (nuint)memory.Length, 320, 240, 1280, 32, 0xFF0000, 0xFF00, 0xFF)); Assert.Throws(() => presenter.Present(new int[4])); } } private static string Text(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text)); }