diff options
Diffstat (limited to 'src/terminal_test.go')
| -rw-r--r-- | src/terminal_test.go | 417 |
1 files changed, 399 insertions, 18 deletions
diff --git a/src/terminal_test.go b/src/terminal_test.go index bb10ec1b..69d5b1ef 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -2,19 +2,16 @@ package fzf import ( "bytes" + "io" + "os" "regexp" + "strings" "testing" "text/template" "github.com/junegunn/fzf/src/util" ) -func newItem(str string) *Item { - bytes := []byte(str) - trimmed, _, _ := extractColor(str, nil, nil) - return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} -} - func TestReplacePlaceholder(t *testing.T) { item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") items1 := []*Item{item1, item1} @@ -34,9 +31,9 @@ func TestReplacePlaceholder(t *testing.T) { } // helper function that converts template format into string and carries out the check() checkFormat := func(format string) { - type quotes struct{ O, I string } // outer, inner quotes - unixStyle := quotes{"'", "'\\''"} - windowsStyle := quotes{"^\"", "'"} + type quotes struct{ O, I, S string } // outer, inner quotes, print separator + unixStyle := quotes{`'`, `'\''`, "\n"} + windowsStyle := quotes{`^"`, `'`, "\n"} var effectiveStyle quotes if util.IsWindows() { @@ -49,6 +46,11 @@ func TestReplacePlaceholder(t *testing.T) { check(expected) } printsep := "\n" + + /* + Test multiple placeholders and the function parameters. + */ + // {}, preserve ansi result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") @@ -135,27 +137,291 @@ func TestReplacePlaceholder(t *testing.T) { // foo'bar baz result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") + + /* + Test single placeholders, but focus on the placeholders' parameters (e.g. flags). + see: TestParsePlaceholder + */ + items3 := []*Item{ + // single line + newItem("1a 1b 1c 1d 1e 1f"), + // multi line + newItem("1a 1b 1c 1d 1e 1f"), + newItem("2a 2b 2c 2d 2e 2f"), + newItem("3a 3b 3c 3d 3e 3f"), + newItem("4a 4b 4c 4d 4e 4f"), + newItem("5a 5b 5c 5d 5e 5f"), + newItem("6a 6b 6c 6d 6e 6f"), + newItem("7a 7b 7c 7d 7e 7f"), + } + stripAnsi := false + printsep = "\n" + forcePlus := false + query := "sample query" + + templateToOutput := make(map[string]string) + templateToFile := make(map[string]string) // same as above, but the file contents will be matched + // I. item type placeholder + templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}` + templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}` + templateToOutput[`{n}`] = `0` + templateToOutput[`{+n}`] = `0 0 0 0 0 0 0` + templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}` + templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}` + templateToFile[`{nf}`] = `0{{.S}}` + templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}` + + // II. token type placeholders + templateToOutput[`{..}`] = templateToOutput[`{}`] + templateToOutput[`{1..}`] = templateToOutput[`{}`] + templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}` + templateToOutput[`{1..2}`] = templateToOutput[`{..2}`] + templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}` + // shorthand for x..x range + templateToOutput[`{1}`] = `{{.O}}1a{{.O}}` + templateToOutput[`{1..1}`] = templateToOutput[`{1}`] + templateToOutput[`{-6}`] = templateToOutput[`{1}`] + // multiple ranges + templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`] + templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}` + templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}` + templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}` + // flags + templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}` + templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}` + templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}` + templateToFile[`{f1}`] = `1a{{.S}}` + templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}` + templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}` + + // III. query type placeholder + // query flag is not removed after parsing, so it gets doubled + // while the double q is invalid, it is useful here for testing purposes + templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" + + // IV. escaping placeholder + templateToOutput[`\{}`] = `{}` + templateToOutput[`\{++}`] = `{++}` + templateToOutput[`{++}`] = templateToOutput[`{+}`] + + for giveTemplate, wantOutput := range templateToOutput { + result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + checkFormat(wantOutput) + } + for giveTemplate, wantOutput := range templateToFile { + path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + + data, err := readFile(path) + if err != nil { + t.Errorf("Cannot read the content of the temp file %s.", path) + } + result = string(data) + + checkFormat(wantOutput) + } } -func TestQuoteEntryCmd(t *testing.T) { +func TestQuoteEntry(t *testing.T) { + type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash + unixStyle := quotes{``, `'`, `'\''`, `"`, `\`} + windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`} + var effectiveStyle quotes + + if util.IsWindows() { + effectiveStyle = windowsStyle + } else { + effectiveStyle = unixStyle + } + tests := map[string]string{ - `"`: `^"\^"^"`, - `\`: `^"\\^"`, - `\"`: `^"\\\^"^"`, - `"\\\"`: `^"\^"\\\\\\\^"^"`, - `&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`, - `%USERPROFILE%`: `^"^%USERPROFILE^%^"`, - `C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`, + `'`: `{{.O}}{{.SQ}}{{.O}}`, + `"`: `{{.O}}{{.DQ}}{{.O}}`, + `\`: `{{.O}}{{.BS}}{{.O}}`, + `\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`, + `"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`, + + `$`: `{{.O}}${{.O}}`, + `$HOME`: `{{.O}}$HOME{{.O}}`, + `'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`, + + `&`: `{{.O}}{{.E}}&{{.O}}`, + `|`: `{{.O}}{{.E}}|{{.O}}`, + `<`: `{{.O}}{{.E}}<{{.O}}`, + `>`: `{{.O}}{{.E}}>{{.O}}`, + `(`: `{{.O}}{{.E}}({{.O}}`, + `)`: `{{.O}}{{.E}}){{.O}}`, + `@`: `{{.O}}{{.E}}@{{.O}}`, + `^`: `{{.O}}{{.E}}^{{.O}}`, + `%`: `{{.O}}{{.E}}%{{.O}}`, + `!`: `{{.O}}{{.E}}!{{.O}}`, + `%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`, + `C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`, + `"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`, } for input, expected := range tests { - escaped := quoteEntryCmd(input) + escaped := quoteEntry(input) + expected = templateToString(expected, effectiveStyle) if escaped != expected { t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) } } } +// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix +func TestUnixCommands(t *testing.T) { + if util.IsWindows() { + t.SkipNow() + } + tests := []testCase{ + // reference: give{template, query, items}, want{output OR match} + + // 1) working examples + + // paths that does not have to evaluated will work fine, when quoted + {give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}}, + {give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}}, + {give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}}, + + // only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory + // quoting the tilde is required (to be treated as string) + {give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}}, + + // 2) problematic examples + + // paths that need to expand some part of it won't work (special characters and variables) + {give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}}, + {give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}}, + } + testCommands(t, tests) +} + +// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows +func TestWindowsCommands(t *testing.T) { + if !util.IsWindows() { + t.SkipNow() + } + tests := []testCase{ + // reference: give{template, query, items}, want{output OR match} + + // 1) working examples + + // example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue + {give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}}, + {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}}, + // example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator + {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}}, + // example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path + {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}}, + + // 2) problematic examples + + // notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12` + {give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}}, + + // cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows + // cat: "C:\\test.txt: Invalid argument + {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}}, + // cat: "C:\\test.txt": Invalid argument + {give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}}, + + // the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it + // the temp file contains: `cat "C:\test.txt"` + {give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}}, + } + testCommands(t, tests) +} + +/* + Test typical valid placeholders and parsing of them. + + Also since the parser assumes the input is matched with `placeholder` regex, + the regex is tested here as well. +*/ +func TestParsePlaceholder(t *testing.T) { + // give, want pairs + templates := map[string]string{ + // I. item type placeholder + `{}`: `{}`, + `{+}`: `{+}`, + `{n}`: `{n}`, + `{+n}`: `{+n}`, + `{f}`: `{f}`, + `{+nf}`: `{+nf}`, + + // II. token type placeholders + `{..}`: `{..}`, + `{1..}`: `{1..}`, + `{..2}`: `{..2}`, + `{1..2}`: `{1..2}`, + `{-2..-1}`: `{-2..-1}`, + // shorthand for x..x range + `{1}`: `{1}`, + `{1..1}`: `{1..1}`, + `{-6}`: `{-6}`, + // multiple ranges + `{1,2}`: `{1,2}`, + `{1,2,4}`: `{1,2,4}`, + `{1,2..4}`: `{1,2..4}`, + `{1..2,-4..-3}`: `{1..2,-4..-3}`, + // flags + `{+1}`: `{+1}`, + `{+-1}`: `{+-1}`, + `{s1}`: `{s1}`, + `{f1}`: `{f1}`, + `{+s1..2}`: `{+s1..2}`, + `{+sf1..2}`: `{+sf1..2}`, + + // III. query type placeholder + // query flag is not removed after parsing, so it gets doubled + // while the double q is invalid, it is useful here for testing purposes + `{q}`: `{qq}`, + + // IV. escaping placeholder + `\{}`: `{}`, + `\{++}`: `{++}`, + `{++}`: `{+}`, + } + + for giveTemplate, wantTemplate := range templates { + if !placeholder.MatchString(giveTemplate) { + t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate) + continue + } + + _, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate) + gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:] + + if gotTemplate != wantTemplate { + t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate) + } + } +} + +/* utilities section */ + +// Item represents one line in fzf UI. Usually it is relative path to files and folders. +func newItem(str string) *Item { + bytes := []byte(str) + trimmed, _, _ := extractColor(str, nil, nil) + return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} +} + +// Functions tested in this file require array of items (allItems). The array needs +// to consist of at least two nils. This is helper function. +func newItems(str ...string) []*Item { + result := make([]*Item, util.Max(len(str), 2)) + for i, s := range str { + result[i] = newItem(s) + } + return result +} + +// (for logging purposes) +func (item *Item) String() string { + return item.AsString(true) +} + // Helper function to parse, execute and convert "text/template" to string. Panics on error. func templateToString(format string, data interface{}) string { bb := &bytes.Buffer{} @@ -167,3 +433,118 @@ func templateToString(format string, data interface{}) string { return bb.String() } + +// ad hoc types for test cases +type give struct { + template string + query string + allItems []*Item +} +type want struct { + /* + Unix: + The `want.output` string is supposed to be formatted for evaluation by + `sh -c command` system call. + + Windows: + The `want.output` string is supposed to be formatted for evaluation by + `cmd.exe /s /c "command"` system call. The `/s` switch enables so called old + behaviour, which is more favourable for nesting (possibly escaped) + special characters. This is the relevant section of `help cmd`: + + ...old behavior is to see if the first character is + a quote character and if so, strip the leading character and + remove the last quote character on the command line, preserving + any text after the last quote character. + */ + output string // literal output + match string // output is matched against this regex (when output is empty string) +} +type testCase struct { + give + want +} + +func testCommands(t *testing.T, tests []testCase) { + // common test parameters + delim := "\t" + delimiter := Delimiter{str: &delim} + printsep := "" + stripAnsi := false + forcePlus := false + + // evaluate the test cases + for idx, test := range tests { + gotOutput := replacePlaceholder( + test.give.template, stripAnsi, delimiter, printsep, forcePlus, + test.give.query, + test.give.allItems) + switch { + case test.want.output != "": + if gotOutput != test.want.output { + t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", + idx, + test.give.template, test.give.query, test.give.allItems, + gotOutput, test.want.output) + } + case test.want.match != "": + wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`) + wantRegex := regexp.MustCompile(wantMatch) + if !wantRegex.MatchString(gotOutput) { + t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", + idx, + test.give.template, test.give.query, test.give.allItems, + gotOutput, test.want.match) + } + default: + t.Errorf("tests[%v]: test case does not describe 'want' property", idx) + } + } +} + +// naive encoder of placeholder flags +func (flags placeholderFlags) encodePlaceholder() string { + encoded := "" + if flags.plus { + encoded += "+" + } + if flags.preserveSpace { + encoded += "s" + } + if flags.number { + encoded += "n" + } + if flags.file { + encoded += "f" + } + if flags.query { + encoded += "q" + } + return encoded +} + +// can be replaced with os.ReadFile() in go 1.16+ +func readFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + data := make([]byte, 0, 128) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + + n, err := file.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} |
