mirror of
https://github.com/taigrr/gopher-os
synced 2025-01-18 04:43:13 -08:00
414 lines
11 KiB
Go
414 lines
11 KiB
Go
package tty
|
|
|
|
import (
|
|
"gopheros/device"
|
|
"gopheros/device/video/console"
|
|
"image/color"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
func TestVtPosition(t *testing.T) {
|
|
specs := []struct {
|
|
inX, inY uint32
|
|
expX, expY uint32
|
|
}{
|
|
{20, 20, 20, 20},
|
|
{100, 20, 80, 20},
|
|
{10, 200, 10, 25},
|
|
{10, 200, 10, 25},
|
|
{100, 100, 80, 25},
|
|
}
|
|
|
|
var term Device = NewVT(4, 0)
|
|
|
|
// SetCursorPosition without an attached console is a no-op
|
|
term.SetCursorPosition(2, 2)
|
|
|
|
if curX, curY := term.CursorPosition(); curX != 1 || curY != 1 {
|
|
t.Fatalf("expected terminal initial position to be (1, 1); got (%d, %d)", curX, curY)
|
|
}
|
|
|
|
cons := newMockConsole(80, 25)
|
|
term.AttachTo(cons)
|
|
|
|
for specIndex, spec := range specs {
|
|
term.SetCursorPosition(spec.inX, spec.inY)
|
|
if x, y := term.CursorPosition(); x != spec.expX || y != spec.expY {
|
|
t.Errorf("[spec %d] expected setting position to (%d, %d) to update the position to (%d, %d); got (%d, %d)", specIndex, spec.inX, spec.inY, spec.expX, spec.expY, x, y)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVtWrite(t *testing.T) {
|
|
t.Run("inactive terminal", func(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
|
|
term := NewVT(4, 0)
|
|
if _, err := term.Write([]byte("foo")); err != io.ErrClosedPipe {
|
|
t.Fatal("expected calling Write on a terminal without an attached console to return ErrClosedPipe")
|
|
}
|
|
|
|
term.AttachTo(cons)
|
|
|
|
term.curFg = 2
|
|
term.curBg = 3
|
|
|
|
data := []byte("\b123\b4\t5\n67\r68")
|
|
count, err := term.Write(data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if count != len(data) {
|
|
t.Fatalf("expected to write %d bytes; wrote %d", len(data), count)
|
|
}
|
|
|
|
if cons.bytesWritten != 0 {
|
|
t.Fatalf("expected writes not to be synced with console when terminal is inactive; %d bytes written", cons.bytesWritten)
|
|
}
|
|
|
|
specs := []struct {
|
|
x, y uint32
|
|
expByte uint8
|
|
}{
|
|
{1, 1, '1'},
|
|
{2, 1, '2'},
|
|
{3, 1, '4'},
|
|
{8, 1, '5'}, // 2 + tabWidth + 1
|
|
{1, 2, '6'},
|
|
{2, 2, '8'},
|
|
}
|
|
|
|
for specIndex, spec := range specs {
|
|
offset := ((spec.y - 1) * term.viewportWidth * 3) + ((spec.x - 1) * 3)
|
|
if term.data[offset] != spec.expByte {
|
|
t.Errorf("[spec %d] expected char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, term.data[offset])
|
|
}
|
|
|
|
if term.data[offset+1] != term.curFg {
|
|
t.Errorf("[spec %d] expected fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, term.data[offset+1])
|
|
}
|
|
|
|
if term.data[offset+2] != term.curBg {
|
|
t.Errorf("[spec %d] expected bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, term.data[offset+2])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("active terminal", func(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
|
|
term := NewVT(4, 0)
|
|
term.SetState(StateActive)
|
|
term.SetState(StateActive) // calling SetState with the same state is a no-op
|
|
|
|
if got := term.State(); got != StateActive {
|
|
t.Fatalf("expected terminal state to be %d; got %d", StateActive, got)
|
|
}
|
|
|
|
term.AttachTo(cons)
|
|
|
|
term.curFg = 2
|
|
term.curBg = 3
|
|
|
|
data := []byte("\b123\b4\t5\n67\r68")
|
|
term.Write(data)
|
|
|
|
if expCount := len(data); cons.bytesWritten != expCount {
|
|
t.Fatalf("expected writes to be synced with console when terminal is active. %d bytes written; expected %d", cons.bytesWritten, expCount)
|
|
}
|
|
|
|
specs := []struct {
|
|
x, y uint32
|
|
expByte uint8
|
|
}{
|
|
{1, 1, '1'},
|
|
{2, 1, '2'},
|
|
{3, 1, '4'},
|
|
{8, 1, '5'}, // 2 + tabWidth + 1
|
|
{1, 2, '6'},
|
|
{2, 2, '8'},
|
|
}
|
|
|
|
for specIndex, spec := range specs {
|
|
offset := ((spec.y - 1) * cons.width) + (spec.x - 1)
|
|
if cons.chars[offset] != spec.expByte {
|
|
t.Errorf("[spec %d] expected console char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, cons.chars[offset])
|
|
}
|
|
|
|
if cons.fgAttrs[offset] != term.curFg {
|
|
t.Errorf("[spec %d] expected console fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, cons.fgAttrs[offset])
|
|
}
|
|
|
|
if cons.bgAttrs[offset] != term.curBg {
|
|
t.Errorf("[spec %d] expected console bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, cons.bgAttrs[offset])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVtLineFeedHandling(t *testing.T) {
|
|
t.Run("viewport at end of terminal", func(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
|
|
term := NewVT(4, 0)
|
|
term.SetState(StateActive)
|
|
term.AttachTo(cons)
|
|
|
|
// Fill last line except the last column which will trigger a
|
|
// line feed. Cursor position will be automatically clipped to
|
|
// the viewport bounds
|
|
term.SetCursorPosition(1, term.viewportHeight+1)
|
|
for i := uint32(0); i < term.viewportWidth-1; i++ {
|
|
term.WriteByte(byte('0' + (i % 10)))
|
|
}
|
|
|
|
// Emulate viewportHeight line feeds. The last one should cause a scroll
|
|
term.SetCursorPosition(0, 0) // cursor is set to (1,1)
|
|
for i := uint32(0); i < term.viewportHeight; i++ {
|
|
term.lf(true)
|
|
}
|
|
|
|
if cons.scrollUpCount != 1 {
|
|
t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount)
|
|
}
|
|
|
|
// Set cursor one line above the last; this line should now
|
|
// contain the scrolled contents
|
|
term.SetCursorPosition(1, term.viewportHeight-1)
|
|
for col, offset := uint32(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
|
expByte := byte('0' + ((col - 1) % 10))
|
|
if col == term.viewportWidth {
|
|
expByte = ' '
|
|
}
|
|
|
|
if term.data[offset] != expByte {
|
|
t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight-1, expByte, term.data[offset])
|
|
}
|
|
}
|
|
|
|
// Set cursor to the last line. This line should now be cleared
|
|
term.SetCursorPosition(1, term.viewportHeight)
|
|
for col, offset := uint32(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
|
expByte := uint8(' ')
|
|
if term.data[offset] != expByte {
|
|
t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight, expByte, term.data[offset])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("viewport not at end of terminal", func(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
|
|
term := NewVT(4, 1)
|
|
term.SetState(StateActive)
|
|
term.AttachTo(cons)
|
|
|
|
// Fill last line except the last column which will trigger a
|
|
// line feed. Cursor position will be automatically clipped to
|
|
// the viewport bounds
|
|
term.SetCursorPosition(1, term.viewportHeight+1)
|
|
for i := uint32(0); i < term.viewportWidth-1; i++ {
|
|
term.WriteByte(byte('0' + (i % 10)))
|
|
}
|
|
|
|
// Fill first line including the last column
|
|
term.SetCursorPosition(1, 1)
|
|
for i := uint32(0); i < term.viewportWidth; i++ {
|
|
term.WriteByte(byte('0' + (i % 10)))
|
|
}
|
|
|
|
// Emulate viewportHeight line feeds. The last one should cause a scroll
|
|
// in the console but only a viewport adjustment in the terminal
|
|
term.SetCursorPosition(0, 0) // cursor is set to (1,1)
|
|
for i := uint32(0); i < term.viewportHeight; i++ {
|
|
term.lf(true)
|
|
}
|
|
|
|
if cons.scrollUpCount != 1 {
|
|
t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount)
|
|
}
|
|
|
|
if expViewportY := uint32(1); term.viewportY != expViewportY {
|
|
t.Fatalf("expected terminal viewportY to be adjusted to %d; got %d", expViewportY, term.viewportY)
|
|
}
|
|
|
|
// Check that first line is still available in the terminal buffer
|
|
// that is not currently visible
|
|
term.SetCursorPosition(1, 1)
|
|
offset := term.dataOffset - uint(term.viewportWidth*3)
|
|
|
|
for col := uint32(1); col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
|
expByte := byte('0' + ((col - 1) % 10))
|
|
|
|
if term.data[offset] != expByte {
|
|
t.Errorf("expected char at hidden region (%d, -1) to be %q; got %q", col, expByte, term.data[offset])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVtAttach(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
|
|
term := NewVT(4, 1)
|
|
|
|
// AttachTo with a nil console should be a no-op
|
|
term.AttachTo(nil)
|
|
if term.termWidth != 0 || term.termHeight != 0 || term.viewportWidth != 0 || term.viewportHeight != 0 {
|
|
t.Fatal("expected attaching a nil console to be a no-op")
|
|
}
|
|
|
|
term.AttachTo(cons)
|
|
if term.termWidth != cons.width ||
|
|
term.termHeight != cons.height+term.scrollback ||
|
|
term.viewportWidth != cons.width ||
|
|
term.viewportHeight != cons.height ||
|
|
term.data == nil {
|
|
t.Fatal("expected the terminal to initialize using the attached console info")
|
|
}
|
|
}
|
|
|
|
func TestVtSetState(t *testing.T) {
|
|
cons := newMockConsole(80, 25)
|
|
term := NewVT(4, 1)
|
|
term.AttachTo(cons)
|
|
|
|
// Fill terminal viewport using a rotating pattern. Writing the last
|
|
// character will cause a scroll operation moving the viewport down
|
|
row := 0
|
|
for index := 0; index < int(term.viewportWidth*term.viewportHeight); index++ {
|
|
if index != 0 && index%int(term.viewportWidth) == 0 {
|
|
row++
|
|
}
|
|
term.curFg = uint8((row + index + 1) % 10)
|
|
term.curBg = uint8((row + index + 2) % 10)
|
|
term.WriteByte(byte('0' + (row+index)%10))
|
|
}
|
|
|
|
// Activating this terminal should trigger a copy of the terminal viewport
|
|
// contents to the console.
|
|
term.SetState(StateActive)
|
|
row = 1
|
|
for index := 0; index < len(cons.chars); index++ {
|
|
if index != 0 && index%int(cons.width) == 0 {
|
|
row++
|
|
}
|
|
|
|
expCh := uint8('0' + (row+index)%10)
|
|
expFg := uint8((row + index + 1) % 10)
|
|
expBg := uint8((row + index + 2) % 10)
|
|
|
|
// last line should be cleared due to the scroll operation
|
|
if row == int(cons.height) {
|
|
expCh = ' '
|
|
expFg = 7
|
|
expBg = 0
|
|
}
|
|
|
|
if cons.chars[index] != expCh {
|
|
t.Errorf("expected console char at index %d to be %q; got %q", index, expCh, cons.chars[index])
|
|
}
|
|
|
|
if cons.fgAttrs[index] != expFg {
|
|
t.Errorf("expected console fg attr at index %d to be %d; got %d", index, expFg, cons.fgAttrs[index])
|
|
}
|
|
|
|
if cons.bgAttrs[index] != expBg {
|
|
t.Errorf("expected console bg attr at index %d to be %d; got %d", index, expBg, cons.bgAttrs[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVTDriverInterface(t *testing.T) {
|
|
var dev device.Driver = NewVT(0, 0)
|
|
|
|
if err := dev.DriverInit(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if dev.DriverName() == "" {
|
|
t.Fatal("DriverName() returned an empty string")
|
|
}
|
|
|
|
if major, minor, patch := dev.DriverVersion(); major+minor+patch == 0 {
|
|
t.Fatal("DriverVersion() returned an invalid version number")
|
|
}
|
|
}
|
|
|
|
func TestVTProbe(t *testing.T) {
|
|
if drv := probeForVT(); drv == nil {
|
|
t.Fatal("expected probeForVT to return a driver")
|
|
}
|
|
}
|
|
|
|
type mockConsole struct {
|
|
width, height uint32
|
|
fg, bg uint8
|
|
chars []uint8
|
|
fgAttrs []uint8
|
|
bgAttrs []uint8
|
|
bytesWritten int
|
|
scrollUpCount int
|
|
scrollDownCount int
|
|
}
|
|
|
|
func newMockConsole(w, h uint32) *mockConsole {
|
|
return &mockConsole{
|
|
width: w,
|
|
height: h,
|
|
fg: 7,
|
|
bg: 0,
|
|
chars: make([]uint8, w*h),
|
|
fgAttrs: make([]uint8, w*h),
|
|
bgAttrs: make([]uint8, w*h),
|
|
}
|
|
}
|
|
|
|
func (cons *mockConsole) Dimensions() (uint32, uint32) {
|
|
return cons.width, cons.height
|
|
}
|
|
|
|
func (cons *mockConsole) DefaultColors() (uint8, uint8) {
|
|
return cons.fg, cons.bg
|
|
}
|
|
|
|
func (cons *mockConsole) Fill(x, y, width, height uint32, fg, bg uint8) {
|
|
yEnd := y + height - 1
|
|
xEnd := x + width - 1
|
|
|
|
for fy := y; fy <= yEnd; fy++ {
|
|
offset := ((fy - 1) * cons.width)
|
|
for fx := x; fx <= xEnd; fx, offset = fx+1, offset+1 {
|
|
cons.chars[offset] = ' '
|
|
cons.fgAttrs[offset] = fg
|
|
cons.bgAttrs[offset] = bg
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cons *mockConsole) Scroll(dir console.ScrollDir, lines uint32) {
|
|
switch dir {
|
|
case console.ScrollDirUp:
|
|
cons.scrollUpCount++
|
|
case console.ScrollDirDown:
|
|
cons.scrollDownCount++
|
|
}
|
|
}
|
|
|
|
func (cons *mockConsole) Palette() color.Palette {
|
|
return nil
|
|
}
|
|
|
|
func (cons *mockConsole) SetPaletteColor(index uint8, color color.RGBA) {
|
|
}
|
|
|
|
func (cons *mockConsole) Write(b byte, fg, bg uint8, x, y uint32) {
|
|
offset := ((y - 1) * cons.width) + (x - 1)
|
|
cons.chars[offset] = b
|
|
cons.fgAttrs[offset] = fg
|
|
cons.bgAttrs[offset] = bg
|
|
cons.bytesWritten++
|
|
}
|