Skip to content

Commit

Permalink
pkg/cli/term: Add readRune, a much simpler alternative to runeReader.
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaq committed Dec 31, 2019
1 parent e8ed51b commit 245b3dc
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 15 deletions.
8 changes: 1 addition & 7 deletions pkg/cli/term/file_reader_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ var (
errTimeout = errors.New("timed out")
)

type byteReaderWithTimeout interface {
// ReadByte reads a single byte from the underlying file. May return
// errStopped to signal that Stop was called during the read.
ReadByte(timeout time.Duration) (byte, error)
}

// A helper for reading from a file.
type fileReader interface {
byteReaderWithTimeout
Expand All @@ -49,7 +43,7 @@ type bReader struct {

const maxNoProgress = 10

func (r *bReader) ReadByte(timeout time.Duration) (byte, error) {
func (r *bReader) ReadByteWithTimeout(timeout time.Duration) (byte, error) {
for {
ready, err := sys.WaitForRead(timeout, r.file, r.rStop)
if err != nil {
Expand Down
16 changes: 8 additions & 8 deletions pkg/cli/term/file_reader_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import (
"time"
)

func TestFileReader_ReadByte(t *testing.T) {
func TestFileReader_ReadByteWithTimeout(t *testing.T) {
r, w, cleanup := setupFileReader()
defer cleanup()

content := []byte("0123456789")
w.Write(content)

// Test successful ReadByte calls.
// Test successful ReadByteWithTimeout calls.
for i := 0; i < len(content); i++ {
t.Run(fmt.Sprintf("byte %d", i), func(t *testing.T) {
b, err := r.ReadByte(-1)
b, err := r.ReadByteWithTimeout(-1)
if err != nil {
t.Errorf("got err %v, want nil", err)
}
Expand All @@ -31,22 +31,22 @@ func TestFileReader_ReadByte(t *testing.T) {
}
}

func TestFileReader_ReadByte_EOF(t *testing.T) {
func TestFileReader_ReadByteWithTimeout_EOF(t *testing.T) {
r, w, cleanup := setupFileReader()
defer cleanup()

w.Close()
_, err := r.ReadByte(-1)
_, err := r.ReadByteWithTimeout(-1)
if err != io.EOF {
t.Errorf("got byte %v, want %v", err, io.EOF)
}
}

func TestFileReader_ReadByte_Timeout(t *testing.T) {
func TestFileReader_ReadByteWithTimeout_Timeout(t *testing.T) {
r, _, cleanup := setupFileReader()
defer cleanup()

_, err := r.ReadByte(time.Millisecond)
_, err := r.ReadByteWithTimeout(time.Millisecond)
if err != errTimeout {
t.Errorf("got err %v, want %v", err, errTimeout)
}
Expand All @@ -58,7 +58,7 @@ func TestFileReader_Stop(t *testing.T) {

errCh := make(chan error, 1)
go func() {
_, err := r.ReadByte(-1)
_, err := r.ReadByteWithTimeout(-1)
errCh <- err
}()
r.Stop()
Expand Down
47 changes: 47 additions & 0 deletions pkg/cli/term/read_rune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package term

import (
"time"
)

type byteReaderWithTimeout interface {
// ReadByteWithTimeout reads a single byte with a timeout. A negative
// timeout means no timeout.
ReadByteWithTimeout(timeout time.Duration) (byte, error)
}

const badRune = '\ufffd'

var utf8SeqTimeout = 10 * time.Millisecond

// Reads a rune from the reader. The timeout applies to the first byte; a
// negative value means no timeout.
func readRune(rd byteReaderWithTimeout, timeout time.Duration) (rune, error) {
leader, err := rd.ReadByteWithTimeout(timeout)
if err != nil {
return badRune, err
}
var r rune
pending := 0
switch {
case leader>>7 == 0:
r = rune(leader)
case leader>>5 == 0x6:
r = rune(leader & 0x1f)
pending = 1
case leader>>4 == 0xe:
r = rune(leader & 0xf)
pending = 2
case leader>>3 == 0x1e:
r = rune(leader & 0x7)
pending = 3
}
for i := 0; i < pending; i++ {
b, err := rd.ReadByteWithTimeout(utf8SeqTimeout)
if err != nil {
return badRune, err
}
r = r<<6 + rune(b&0x3f)
}
return r, nil
}
62 changes: 62 additions & 0 deletions pkg/cli/term/read_rune_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// +build !windows,!plan9

package term

import "testing"

// TODO(xiaq): Do not depend on Unix for this test.

var contents = []string{
"English",
"Ελληνικά",
"你好 こんにちは",
"𐌰𐌱",
}

func TestReadRune(t *testing.T) {
for _, content := range contents {
t.Run(content, func(t *testing.T) {
rd, w, cleanup := setupFileReader()
defer cleanup()

w.Write([]byte(content))
for _, wantRune := range []rune(content) {
r, err := readRune(rd, 0)
if r != wantRune {
t.Errorf("got rune %q, want %q", r, wantRune)
}
if err != nil {
t.Errorf("got err %v, want nil", err)
}
}
})
}
}

func TestReadRune_ErrorAtFirstByte(t *testing.T) {
rd, _, cleanup := setupFileReader()
defer cleanup()

r, err := readRune(rd, 0)
if r != '\ufffd' {
t.Errorf("got rune %q, want %q", r, '\ufffd')
}
if err == nil {
t.Errorf("got err %v, want non-nil", err)
}
}

func TestReadRune_ErrorAtNonFirstByte(t *testing.T) {
rd, w, cleanup := setupFileReader()
defer cleanup()

w.Write([]byte{0xe4})

r, err := readRune(rd, 0)
if r != '\ufffd' {
t.Errorf("got rune %q, want %q", r, '\ufffd')
}
if err == nil {
t.Errorf("got err %v, want non-nil", err)
}
}

0 comments on commit 245b3dc

Please sign in to comment.