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<ArgumentException>(()=>framebuffer.Present(ReadOnlySpan<int>.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<int> 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<ArgumentException>(() => new ConsoleMemoryFramebuffer(new(address, (nuint)memory.Length, 320, 240, 1280, 32, 0xFF00, 0xFF00, 0xFF)));
Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => presenter.Present(new int[4]));
}
}
private static string Text(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text));
}
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<ArgumentException>(()=>framebuffer.Present(ReadOnlySpan<int>.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<int> 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<ArgumentException>(() => new ConsoleMemoryFramebuffer(new(address, (nuint)memory.Length, 320, 240, 1280, 32, 0xFF00, 0xFF00, 0xFF)));
Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => presenter.Present(new int[4]));
}
}
private static string Text(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text));
}