Files

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)
}
}