github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/term/reader_unix_test.go (about)

     1  //go:build !windows && !plan9
     2  // +build !windows,!plan9
     3  
     4  package term
     5  
     6  import (
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/markusbkk/elvish/pkg/testutil"
    12  	"github.com/markusbkk/elvish/pkg/ui"
    13  )
    14  
    15  var readEventTests = []struct {
    16  	input string
    17  	want  Event
    18  }{
    19  	// Simple graphical key.
    20  	{"x", K('x')},
    21  	{"X", K('X')},
    22  	{" ", K(' ')},
    23  
    24  	// Ctrl key.
    25  	{"\001", K('A', ui.Ctrl)},
    26  	{"\033", K('[', ui.Ctrl)},
    27  
    28  	// Special Ctrl keys that do not obey the usual 0x40 rule.
    29  	{"\000", K('`', ui.Ctrl)},
    30  	{"\x1e", K('6', ui.Ctrl)},
    31  	{"\x1f", K('/', ui.Ctrl)},
    32  
    33  	// Ambiguous Ctrl keys; the reader uses the non-Ctrl form as canonical.
    34  	{"\n", K('\n')},
    35  	{"\t", K('\t')},
    36  	{"\x7f", K('\x7f')}, // backspace
    37  
    38  	// Alt plus simple graphical key.
    39  	{"\033a", K('a', ui.Alt)},
    40  	{"\033[", K('[', ui.Alt)},
    41  
    42  	// G3-style key.
    43  	{"\033OA", K(ui.Up)},
    44  	{"\033OH", K(ui.Home)},
    45  
    46  	// G3-style key with leading Escape.
    47  	{"\033\033OA", K(ui.Up, ui.Alt)},
    48  	{"\033\033OH", K(ui.Home, ui.Alt)},
    49  
    50  	// Alt-O. This is handled as a special case because it looks like a G3-style
    51  	// key.
    52  	{"\033O", K('O', ui.Alt)},
    53  
    54  	// CSI-sequence key identified by the ending rune.
    55  	{"\033[A", K(ui.Up)},
    56  	{"\033[H", K(ui.Home)},
    57  	// Modifiers.
    58  	{"\033[1;0A", K(ui.Up)},
    59  	{"\033[1;1A", K(ui.Up)},
    60  	{"\033[1;2A", K(ui.Up, ui.Shift)},
    61  	{"\033[1;3A", K(ui.Up, ui.Alt)},
    62  	{"\033[1;4A", K(ui.Up, ui.Shift, ui.Alt)},
    63  	{"\033[1;5A", K(ui.Up, ui.Ctrl)},
    64  	{"\033[1;6A", K(ui.Up, ui.Shift, ui.Ctrl)},
    65  	{"\033[1;7A", K(ui.Up, ui.Alt, ui.Ctrl)},
    66  	{"\033[1;8A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)},
    67  	// The modifiers below should be for Meta, but we conflate Alt and Meta.
    68  	{"\033[1;9A", K(ui.Up, ui.Alt)},
    69  	{"\033[1;10A", K(ui.Up, ui.Shift, ui.Alt)},
    70  	{"\033[1;11A", K(ui.Up, ui.Alt)},
    71  	{"\033[1;12A", K(ui.Up, ui.Shift, ui.Alt)},
    72  	{"\033[1;13A", K(ui.Up, ui.Alt, ui.Ctrl)},
    73  	{"\033[1;14A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)},
    74  	{"\033[1;15A", K(ui.Up, ui.Alt, ui.Ctrl)},
    75  	{"\033[1;16A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)},
    76  
    77  	// CSI-sequence key with one argument, ending in '~'.
    78  	{"\033[1~", K(ui.Home)},
    79  	{"\033[11~", K(ui.F1)},
    80  	// Modified.
    81  	{"\033[1;2~", K(ui.Home, ui.Shift)},
    82  	// Urxvt-flavor modifier, shifting the '~' to reflect the modifier
    83  	{"\033[1$", K(ui.Home, ui.Shift)},
    84  	{"\033[1^", K(ui.Home, ui.Ctrl)},
    85  	{"\033[1@", K(ui.Home, ui.Shift, ui.Ctrl)},
    86  	// With a leading Escape.
    87  	{"\033\033[1~", K(ui.Home, ui.Alt)},
    88  
    89  	// CSI-sequence key with three arguments and ending in '~'. The first
    90  	// argument is always 27, the second identifies the modifier and the last
    91  	// identifies the key.
    92  	{"\033[27;4;63~", K(';', ui.Shift, ui.Alt)},
    93  
    94  	// Cursor Position Report.
    95  	{"\033[3;4R", CursorPosition{3, 4}},
    96  
    97  	// Paste setting.
    98  	{"\033[200~", PasteSetting(true)},
    99  	{"\033[201~", PasteSetting(false)},
   100  
   101  	// Mouse event.
   102  	{"\033[M\x00\x23\x24", MouseEvent{Pos{4, 3}, true, 0, 0}},
   103  	// Other buttons.
   104  	{"\033[M\x01\x23\x24", MouseEvent{Pos{4, 3}, true, 1, 0}},
   105  	// Button up.
   106  	{"\033[M\x03\x23\x24", MouseEvent{Pos{4, 3}, false, -1, 0}},
   107  	// Modified.
   108  	{"\033[M\x04\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Shift}},
   109  	{"\033[M\x08\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Alt}},
   110  	{"\033[M\x10\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Ctrl}},
   111  	{"\033[M\x14\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Shift | ui.Ctrl}},
   112  
   113  	// SGR-style mouse event.
   114  	{"\033[<0;3;4M", MouseEvent{Pos{4, 3}, true, 0, 0}},
   115  	// Other buttons.
   116  	{"\033[<1;3;4M", MouseEvent{Pos{4, 3}, true, 1, 0}},
   117  	// Button up.
   118  	{"\033[<0;3;4m", MouseEvent{Pos{4, 3}, false, 0, 0}},
   119  	// Modified.
   120  	{"\033[<4;3;4M", MouseEvent{Pos{4, 3}, true, 0, ui.Shift}},
   121  	{"\033[<16;3;4M", MouseEvent{Pos{4, 3}, true, 0, ui.Ctrl}},
   122  }
   123  
   124  func TestReader_ReadEvent(t *testing.T) {
   125  	r, w := setupReader(t)
   126  
   127  	for _, test := range readEventTests {
   128  		t.Run(test.input, func(t *testing.T) {
   129  			w.WriteString(test.input)
   130  			ev, err := r.ReadEvent()
   131  			if ev != test.want {
   132  				t.Errorf("got event %v, want %v", ev, test.want)
   133  			}
   134  			if err != nil {
   135  				t.Errorf("got err %v, want %v", err, nil)
   136  			}
   137  		})
   138  	}
   139  }
   140  
   141  var readEventBadSeqTests = []struct {
   142  	input      string
   143  	wantErrMsg string
   144  }{
   145  	// mouse event should have exactly 3 bytes after \033[M
   146  	{"\033[M", "incomplete mouse event"},
   147  	{"\033[M1", "incomplete mouse event"},
   148  	{"\033[M12", "incomplete mouse event"},
   149  
   150  	// CSI needs to be terminated by something that is not a parameter
   151  	{"\033[1", "incomplete CSI"},
   152  	{"\033[;", "incomplete CSI"},
   153  	{"\033[1;", "incomplete CSI"},
   154  
   155  	// CPR should have exactly 2 parameters
   156  	{"\033[1R", "bad CPR"},
   157  	{"\033[1;2;3R", "bad CPR"},
   158  
   159  	// SGR mouse event should have exactly 3 parameters
   160  	{"\033[<1;2m", "bad SGR mouse event"},
   161  
   162  	// csiSeqByLast should have 0 or 2 parameters
   163  	{"\033[1;2;3A", "bad CSI"},
   164  	// csiSeqByLast with 2 parameters should have first parameter = 1
   165  	{"\033[2;1A", "bad CSI"},
   166  	// xterm-style modifier should be 0 to 16
   167  	{"\033[1;17A", "bad CSI"},
   168  	// unknown CSI terminator
   169  	{"\033[x", "bad CSI"},
   170  
   171  	// G3 allows a small list of allowed bytes after \033O
   172  	{"\033Ox", "bad G3"},
   173  }
   174  
   175  func TestReader_ReadEvent_BadSeq(t *testing.T) {
   176  	r, w := setupReader(t)
   177  
   178  	for _, test := range readEventBadSeqTests {
   179  		t.Run(test.input, func(t *testing.T) {
   180  			w.WriteString(test.input)
   181  			ev, err := r.ReadEvent()
   182  			if err == nil {
   183  				t.Fatalf("got nil err with event %v, want non-nil error", ev)
   184  			}
   185  			errMsg := err.Error()
   186  			if !strings.HasPrefix(errMsg, test.wantErrMsg) {
   187  				t.Errorf("got err with message %v, want message starting with %v",
   188  					errMsg, test.wantErrMsg)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func TestReader_ReadRawEvent(t *testing.T) {
   195  	rd, w := setupReader(t)
   196  
   197  	for _, test := range readEventTests {
   198  		input := test.input
   199  		t.Run(input, func(t *testing.T) {
   200  			w.WriteString(input)
   201  			for _, r := range input {
   202  				ev, err := rd.ReadRawEvent()
   203  				if err != nil {
   204  					t.Errorf("got error %v, want nil", err)
   205  				}
   206  				if ev != K(r) {
   207  					t.Errorf("got event %v, want %v", ev, K(r))
   208  				}
   209  			}
   210  		})
   211  	}
   212  }
   213  
   214  func setupReader(t *testing.T) (Reader, *os.File) {
   215  	pr, pw := testutil.MustPipe()
   216  	r := NewReader(pr)
   217  	t.Cleanup(func() {
   218  		r.Close()
   219  		pr.Close()
   220  		pw.Close()
   221  	})
   222  	return r, pw
   223  }