223 lines
6.3 KiB
Go
223 lines
6.3 KiB
Go
package braille
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestViewportUnicodePanningAndReplacement(t *testing.T) {
|
|
v := viewport{width: 4}
|
|
v.replace("é界abcde")
|
|
if got := v.frame(); got != "é界ab" {
|
|
t.Fatalf("first frame: %q", got)
|
|
}
|
|
if v.pan(PanLeft) {
|
|
t.Fatal("panned before beginning")
|
|
}
|
|
if !v.pan(PanRight) || v.frame() != "cde " {
|
|
t.Fatalf("second frame: %q", v.frame())
|
|
}
|
|
if v.pan(PanRight) {
|
|
t.Fatal("panned past end")
|
|
}
|
|
if !v.pan(PanLeft) || v.frame() != "é界ab" {
|
|
t.Fatal("could not return to first frame")
|
|
}
|
|
v.pan(PanRight)
|
|
v.replace("x")
|
|
if v.offset != 0 || v.frame() != "x " {
|
|
t.Fatalf("replacement retained old cells: %q", v.frame())
|
|
}
|
|
v.replace("")
|
|
if v.frame() != " " {
|
|
t.Fatal("empty frame was not cleared")
|
|
}
|
|
v.replace("\x00\n\t\x7f")
|
|
if v.frame() != " " {
|
|
t.Fatal("control characters reached display")
|
|
}
|
|
}
|
|
|
|
func TestViewportWordWrapAndIndicators(t *testing.T) {
|
|
v := viewport{width: 6}
|
|
if err := v.setFrame(Frame{Text: "one two three", Cursor: -1, WordWrap: true,
|
|
Dots: []byte{0, 0, 0, 0, 0x40, 0x40, 0, 0, 0, 0, 0, 0, 0}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.frame() != "one " {
|
|
t.Fatalf("first wrapped frame=%q", v.frame())
|
|
}
|
|
if !v.pan(PanRight) || v.frame() != "two " || v.offset != 4 || v.displayDots()[0] != 0x40 {
|
|
t.Fatalf("wrapped frame=%q offset=%d dots=%v", v.frame(), v.offset, v.displayDots())
|
|
}
|
|
if !v.pan(PanLeft) || v.offset != 0 {
|
|
t.Fatalf("left offset=%d", v.offset)
|
|
}
|
|
if err := v.setFrame(Frame{Text: "abcdefghij", Cursor: -1, WordWrap: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !v.pan(PanRight) || v.offset != 6 {
|
|
t.Fatalf("long word offset=%d", v.offset)
|
|
}
|
|
}
|
|
|
|
func TestViewportCursorFollowsCaretAndPanning(t *testing.T) {
|
|
v := viewport{width: 4}
|
|
if err := v.setFrame(Frame{Text: "é界abcde", Cursor: 5}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.frame() != "cde " || v.displayCursor() != 2 {
|
|
t.Fatalf("caret page: frame=%q cursor=%d", v.frame(), v.displayCursor())
|
|
}
|
|
if !v.pan(PanLeft) || v.displayCursor() != 0 {
|
|
t.Fatal("cursor remained visible outside the panned viewport")
|
|
}
|
|
if !v.pan(PanRight) || v.displayCursor() != 2 {
|
|
t.Fatal("cursor did not reappear on its page")
|
|
}
|
|
if err := v.setFrame(Frame{Text: "abcd", Cursor: 4}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.frame() != " " || v.displayCursor() != 1 {
|
|
t.Fatalf("end-of-line cell: frame=%q cursor=%d", v.frame(), v.displayCursor())
|
|
}
|
|
for _, cursor := range []int{-2, 5} {
|
|
if err := v.setFrame(Frame{Text: "abcd", Cursor: cursor}); err == nil {
|
|
t.Fatalf("accepted cursor %d", cursor)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGeometry(t *testing.T) {
|
|
for _, g := range []Geometry{{0, 1}, {20, 0}, {-1, 1}, {4097, 1}, {1, 4097}, {4096, 4096}} {
|
|
if _, err := g.Cells(); err == nil {
|
|
t.Errorf("accepted %+v", g)
|
|
}
|
|
}
|
|
if cells, err := (Geometry{20, 2}).Cells(); err != nil || cells != 40 {
|
|
t.Fatalf("multirow: %d, %v", cells, err)
|
|
}
|
|
}
|
|
|
|
type fakeBackend struct {
|
|
geometry Geometry
|
|
openErr, writeErr, readErr, closeErr error
|
|
opens, closes int
|
|
writes []string
|
|
cursors []int
|
|
keys []Key
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (f *fakeBackend) Open() (Geometry, error) { f.opens++; return f.geometry, f.openErr }
|
|
func (f *fakeBackend) Write(text string, cursor int) error {
|
|
f.writes = append(f.writes, text)
|
|
f.cursors = append(f.cursors, cursor)
|
|
return f.writeErr
|
|
}
|
|
func (f *fakeBackend) ReadKey() (Key, error) {
|
|
if len(f.keys) > 0 {
|
|
key := f.keys[0]
|
|
f.keys = f.keys[1:]
|
|
return key, f.readErr
|
|
}
|
|
if f.cancel != nil {
|
|
f.cancel()
|
|
}
|
|
return NoKey, f.readErr
|
|
}
|
|
func (f *fakeBackend) Close() error { f.closes++; return f.closeErr }
|
|
|
|
func TestDiagnosticPansAndReleases(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
f := &fakeBackend{geometry: Geometry{20, 1}, keys: []Key{PanRight, PanLeft}, cancel: cancel}
|
|
var output bytes.Buffer
|
|
if err := Diagnostic(ctx, f, &output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.opens != 1 || f.closes != 1 || len(f.writes) != 3 {
|
|
t.Fatalf("lifecycle: %+v", f)
|
|
}
|
|
if f.writes[0] != f.writes[2] || f.writes[0] == f.writes[1] {
|
|
t.Fatalf("pan frames: %q", f.writes)
|
|
}
|
|
if !strings.Contains(output.String(), "20 columns, 1 rows") {
|
|
t.Fatal(output.String())
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticFailureCleanup(t *testing.T) {
|
|
failure := errors.New("fake failure")
|
|
for _, stage := range []string{"open", "geometry", "write", "read", "close"} {
|
|
t.Run(stage, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
f := &fakeBackend{geometry: Geometry{20, 1}, cancel: cancel}
|
|
switch stage {
|
|
case "open":
|
|
f.openErr = failure
|
|
case "geometry":
|
|
f.geometry = Geometry{}
|
|
case "write":
|
|
f.writeErr = failure
|
|
case "read":
|
|
f.readErr = failure
|
|
case "close":
|
|
f.closeErr = failure
|
|
}
|
|
err := Diagnostic(ctx, f, io.Discard)
|
|
if err == nil {
|
|
t.Fatal("failure was lost")
|
|
}
|
|
if stage != "geometry" && !errors.Is(err, failure) {
|
|
t.Fatalf("lost cause: %v", err)
|
|
}
|
|
if f.closes != 1 {
|
|
t.Fatalf("close count: %d", f.closes)
|
|
}
|
|
if (stage == "open" || stage == "geometry") && len(f.writes) > 0 {
|
|
t.Fatal("wrote without a valid display")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticCanceledBeforeOpen(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
f := &fakeBackend{}
|
|
if err := Diagnostic(ctx, f, io.Discard); !errors.Is(err, context.Canceled) {
|
|
t.Fatal(err)
|
|
}
|
|
if f.opens != 0 {
|
|
t.Fatal("opened after cancellation")
|
|
}
|
|
}
|
|
|
|
func TestViewportRoutingRejectsPaddingAndTracksReplacement(t *testing.T) {
|
|
v := viewport{width: 4}
|
|
if err := v.setFrame(Frame{ID: 1, Text: "é猫abc", Cursor: -1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.pan(PanRight)
|
|
if got, ok := v.route(RouteBase); !ok || got != (RoutingEvent{ID: 1, Offset: 4}) {
|
|
t.Fatalf("route %+v %v", got, ok)
|
|
}
|
|
for _, key := range []Key{PanLeft, RouteBase + 1, RouteBase + 4, RouteBase - 1} {
|
|
if _, ok := v.route(key); ok {
|
|
t.Fatalf("routed invalid cell %d", key)
|
|
}
|
|
}
|
|
if err := v.setFrame(Frame{ID: 2, Text: "new", Cursor: -1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, ok := v.route(RouteBase + 1); !ok || got != (RoutingEvent{ID: 2, Offset: 1}) {
|
|
t.Fatalf("replacement route %+v %v", got, ok)
|
|
}
|
|
}
|