github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/terminalescaper/escaper_test.go (about)

     1  package terminalescaper
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"reflect"
     7  	"testing"
     8  	"unicode"
     9  	"unsafe"
    10  )
    11  
    12  var tests = map[string]string{
    13  
    14  	// The vt100 escape character \033 (i.e. \x1b) is substituted with '^', even as part of escape sequence
    15  	"\x1b":          "^[",
    16  	"aaa\x1b[3Gbbb": "aaa^[[3Gbbb",
    17  	"a\033[12laa":   "a^[[12laa",
    18  	// character movement
    19  	"aaa\033[2Db":        "aaa^[[2Db",
    20  	"aaa\033[4D\033[2Cb": "aaa^[[4D^[[2Cb",
    21  	// color
    22  	"aaa \033[25;25mtest":                 "aaa ^[[25;25mtest",
    23  	"bbb \033]4;1;rgb:38/54/71\033\\test": "bbb ^[]4;1;rgb:38/54/71^[\\test",
    24  	"ccc \033]4;1;rgb:38/54/71test":       "ccc ^[]4;1;rgb:38/54/71test",
    25  
    26  	// '\' and '/' are preserved
    27  	"bbb\\raaa": "bbb\\raaa",
    28  	"bbb/raaa":  "bbb/raaa",
    29  
    30  	// newline and tab are preserved, even in combination with other escpae codes
    31  	"\n":                 "\n",
    32  	"\t":                 "\t",
    33  	"bbb\naaa":           "bbb\naaa",
    34  	"bbb\taaa":           "bbb\taaa",
    35  	"b\naaa\b\b\033[4P":  "b\naaa^[[4P",
    36  	"x\naaa\b\b\033[2Ka": "x\naaa^[[2Ka",
    37  
    38  	// valid non ASCII characters
    39  	"⌘":     "⌘",
    40  	"⌘a\n⌘": "⌘a\n⌘",
    41  
    42  	// backspace, carriage return and other similar special characters (except for \n, \t) are stripped out
    43  	"aaa\b\bb":       "aaab",
    44  	"aaa\b\b\033[1K": "aaa^[[1K",
    45  	"bbb\raaa":       "bbbaaa", // carriage return
    46  
    47  	// Colors are acceptable, including multiple in the same string,
    48  	"foo\x1b[30mbar": "foo\x1b[30mbar",
    49  
    50  	// But non-color escapes should be escaped properly
    51  	"fo\x1b[4Po\x1b[30mbarf\x1b[34moobarfo\x1b[0mobarfoobar\x1b312": "fo^[[4Po\x1b[30mbarf\x1b[34moobarfo\x1b[0mobarfoobar^[312",
    52  
    53  	// Edge-cases with colors
    54  	"foo\x1b[30mbar\x1b":         "foo\x1b[30mbar^[",
    55  	"\x1bfoo\x1b[30mbar\x1b":     "^[foo\x1b[30mbar^[",
    56  	"\x1bfoo\x1b[30mbar\x1b[36":  "^[foo\x1b[30mbar^[[36",
    57  	"\x1bfoo\x1b[30mbar\x1b[36m": "^[foo\x1b[30mbar\x1b[36m",
    58  }
    59  
    60  func TestMain(t *testing.T) {
    61  	for a, b := range tests {
    62  		tmp := Clean(a)
    63  		if tmp != b {
    64  			t.Logf("Clean() failed: %#v -> %#v != %#v\n", a, tmp, b)
    65  			t.Fail()
    66  		}
    67  	}
    68  }
    69  
    70  func TestWriter(t *testing.T) {
    71  	var c bytes.Buffer
    72  	w := &Writer{Writer: &c}
    73  
    74  	for a, b := range tests {
    75  		c.Reset()
    76  		n, err := w.Write([]byte(a))
    77  		if c.String() != b {
    78  			t.Logf("Write failed: %#v -> %#v != %#v\n", a, c.String(), b)
    79  			t.Fail()
    80  		}
    81  		if err != nil {
    82  			t.Logf("Write for %#v failed: %#v \n", a, err)
    83  			t.Fail()
    84  		}
    85  		if n != len([]byte(a)) {
    86  			t.Logf("Write for %#v returned wrong length: %#v \n", a, n)
    87  			t.Fail()
    88  		}
    89  	}
    90  }
    91  
    92  func TestWriterStopsOnErr(t *testing.T) {
    93  	w := &Writer{Writer: &mockWriter{}}
    94  
    95  	a := []byte("test")
    96  
    97  	if n, err := w.Write(a); n != len(a) || err != nil {
    98  		t.Logf("Write returned unexpected output: %#v, %#v \n", n, err)
    99  		t.Fail()
   100  	}
   101  
   102  	// This write should fail, as the mock writer errors
   103  	if n, err := w.Write(a); n != 0 || err == nil {
   104  		t.Logf("Write returned unexpected output: %#v, %#v \n", n, err)
   105  		t.Fail()
   106  	}
   107  
   108  	// This write should fail, even if the mock writer would allow it
   109  	if n, err := w.Write(a); n != 0 || err == nil {
   110  		t.Logf("Write returned unexpected output: %#v, %#v \n", n, err)
   111  		t.Fail()
   112  	}
   113  }
   114  
   115  // Allows even calls, errors on odd calls
   116  type mockWriter struct {
   117  	i int
   118  }
   119  
   120  func (m *mockWriter) Write(p []byte) (n int, err error) {
   121  	if m.i == 0 {
   122  		m.i++
   123  		return len(p), nil
   124  	}
   125  
   126  	m.i--
   127  	return 1, fmt.Errorf("error writing")
   128  }
   129  
   130  // Tests for the replace function are adapted from the ones for strings.Map
   131  
   132  func tenRunes(ch rune) string {
   133  	r := make([]rune, 10)
   134  	for i := range r {
   135  		r[i] = ch
   136  	}
   137  	return string(r)
   138  }
   139  
   140  // User-defined self-inverse mapping function
   141  func rot13(r rune) rune {
   142  	step := rune(13)
   143  	if r >= 'a' && r <= 'z' {
   144  		return ((r - 'a' + step) % 26) + 'a'
   145  	}
   146  	if r >= 'A' && r <= 'Z' {
   147  		return ((r - 'A' + step) % 26) + 'A'
   148  	}
   149  	return r
   150  }
   151  func Test_replace(t *testing.T) {
   152  	// Run a couple of awful growth/shrinkage tests
   153  	a := tenRunes('a')
   154  	// 1.  Grow. This triggers two reallocations in Map.
   155  	maxRune := func(rune) rune { return unicode.MaxRune }
   156  	m := replace(maxRune, a)
   157  	expect := tenRunes(unicode.MaxRune)
   158  	if m != expect {
   159  		t.Errorf("growing: expected %q got %q", expect, m)
   160  	}
   161  
   162  	// 2. Shrink
   163  	minRune := func(rune) rune { return 'a' }
   164  	m = replace(minRune, tenRunes(unicode.MaxRune))
   165  	expect = a
   166  	if m != expect {
   167  		t.Errorf("shrinking: expected %q got %q", expect, m)
   168  	}
   169  
   170  	// 3. Rot13
   171  	m = replace(rot13, "a to zed")
   172  	expect = "n gb mrq"
   173  	if m != expect {
   174  		t.Errorf("rot13: expected %q got %q", expect, m)
   175  	}
   176  
   177  	// 4. Rot13^2
   178  	m = replace(rot13, replace(rot13, "a to zed"))
   179  	expect = "a to zed"
   180  	if m != expect {
   181  		t.Errorf("rot13: expected %q got %q", expect, m)
   182  	}
   183  
   184  	// 5. Drop^[
   185  	dropNotLatin := func(r rune) rune {
   186  		if unicode.Is(unicode.Latin, r) {
   187  			return r
   188  		}
   189  		return -2
   190  	}
   191  	m = replace(dropNotLatin, "Hello, 세계")
   192  	expect = "Hello"
   193  	if m != expect {
   194  		t.Errorf("drop: expected %q got %q", expect, m)
   195  	}
   196  
   197  	// 6. Identity
   198  	identity := func(r rune) rune {
   199  		return r
   200  	}
   201  	orig := "Input string that we expect not to be copied."
   202  	m = replace(identity, orig)
   203  	if (*reflect.StringHeader)(unsafe.Pointer(&orig)).Data !=
   204  		(*reflect.StringHeader)(unsafe.Pointer(&m)).Data {
   205  		t.Error("unexpected copy during identity map")
   206  	}
   207  
   208  	// 7. Handle invalid UTF-8 sequence
   209  	replaceNotLatin := func(r rune) rune {
   210  		if unicode.Is(unicode.Latin, r) {
   211  			return r
   212  		}
   213  		return '?'
   214  	}
   215  	m = replace(replaceNotLatin, "Hello\255World")
   216  	expect = "Hello?World"
   217  	if m != expect {
   218  		t.Errorf("replace invalid sequence: expected %q got %q", expect, m)
   219  	}
   220  
   221  	// 8. Handle special case of -1 to '^['
   222  	aToEscape := func(r rune) rune {
   223  		if r == 'a' {
   224  			return -1
   225  		}
   226  		return r
   227  	}
   228  	m = replace(aToEscape, "a")
   229  	expect = "^["
   230  	if m != expect {
   231  		t.Errorf("Escaping: expected %q got %q", expect, m)
   232  	}
   233  	m = replace(aToEscape, "aa")
   234  	expect = "^[^["
   235  	if m != expect {
   236  		t.Errorf("Escaping: expected %q got %q", expect, m)
   237  	}
   238  	m = replace(aToEscape, "abaaba")
   239  	expect = "^[b^[^[b^["
   240  	if m != expect {
   241  		t.Errorf("Escaping: expected %q got %q", expect, m)
   242  	}
   243  }