mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 22:28:24 +02:00
372 lines
11 KiB
Go
372 lines
11 KiB
Go
package choose_files
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"math/rand"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
func TestAsLower(t *testing.T) {
|
|
buf := [512]byte{}
|
|
for _, q := range []string{
|
|
"abc", "aBc", "aBCCf83Dx", "mOoseÇa", "89ÇĞxxA", "", "23", "aIİBc",
|
|
} {
|
|
n := as_lower(q, buf[:])
|
|
actual := utils.UnsafeBytesToString(buf[:n])
|
|
if diff := cmp.Diff(strings.ToLower(q), actual); diff != "" {
|
|
t.Fatalf("Failed to lowercase: %#v\n%s", q, diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
type node struct {
|
|
name string
|
|
children map[string]*node
|
|
data string
|
|
}
|
|
|
|
func (n node) Name() string {
|
|
return n.name
|
|
}
|
|
|
|
func (n node) IsDir() bool {
|
|
return n.children != nil
|
|
}
|
|
|
|
func (n node) String() string {
|
|
return fmt.Sprintf("{name: %s num_children: %d}", n.name, len(n.children))
|
|
}
|
|
|
|
func (n node) Type() fs.FileMode {
|
|
if n.children == nil {
|
|
return 0
|
|
}
|
|
return fs.ModeDir
|
|
}
|
|
|
|
func (n node) Info() (fs.FileInfo, error) {
|
|
return nil, fmt.Errorf("Info() not implemented")
|
|
}
|
|
|
|
func random_name(r *rand.Rand) string {
|
|
length := 3 + r.Intn(23)
|
|
bytes := make([]byte, length)
|
|
for i := range length {
|
|
bytes[i] = byte(r.Intn(26) + 'a')
|
|
}
|
|
return string(bytes)
|
|
}
|
|
|
|
func (n *node) generate_random_tree(depth, breadth int) {
|
|
r := rand.New(rand.NewSource(111))
|
|
n.children = make(map[string]*node)
|
|
for range breadth {
|
|
c := &node{name: random_name(r)}
|
|
n.children[c.name] = c
|
|
if depth > 0 && r.Intn(10) < 3 {
|
|
c.generate_random_tree(depth-1, breadth)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n node) dir_entries() []fs.DirEntry {
|
|
entries := make([]fs.DirEntry, 0, len(n.children))
|
|
for _, v := range n.children {
|
|
entries = append(entries, v)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func (n node) ReadFile(name string) ([]byte, error) {
|
|
if name == string(os.PathSeparator) {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
p := &n
|
|
for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
|
|
c, found := p.children[x]
|
|
if !found {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
p = c
|
|
}
|
|
return []byte(p.data), nil
|
|
}
|
|
|
|
func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
|
|
if name == string(os.PathSeparator) {
|
|
return n.dir_entries(), nil
|
|
}
|
|
p := &n
|
|
for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
|
|
c, found := p.children[x]
|
|
if !found {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
if !c.IsDir() {
|
|
return nil, fs.ErrExist
|
|
}
|
|
p = c
|
|
}
|
|
return p.dir_entries(), nil
|
|
}
|
|
|
|
func TestChooseFilesIgnore(t *testing.T) {
|
|
root := node{name: string(os.PathSeparator), children: map[string]*node{
|
|
"a": {name: "a"},
|
|
"b": {name: "b"},
|
|
"c.png": {name: "c.png"},
|
|
".ignore": {name: ".ignore", data: "a\nx/s/n"},
|
|
".gitignore": {name: ".gitignore", data: "b"},
|
|
"x": {name: "x", children: map[string]*node{
|
|
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
|
|
"s": {name: "s", children: map[string]*node{
|
|
"m": {name: "m"}, "n": {name: "n"},
|
|
}},
|
|
}},
|
|
"y": {name: "y", children: map[string]*node{
|
|
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
|
|
"s": {name: "s", children: map[string]*node{
|
|
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"}, "6": {name: "6"},
|
|
}},
|
|
".gitignore": {name: ".gitignore", data: "/s/5"},
|
|
".git": {name: ".git", children: map[string]*node{
|
|
"info": {name: "info", children: map[string]*node{
|
|
"exclude": {name: "exclude", data: "s/4"},
|
|
}},
|
|
}},
|
|
}},
|
|
}}
|
|
r := func(respect_ignores bool, expected string) {
|
|
ch := make(chan bool)
|
|
s := new_filesystem_scanner("/", ch, nil)
|
|
s.dir_reader = root.ReadDir
|
|
s.file_reader = root.ReadFile
|
|
s.global_gitignore = ignorefiles.NewGitignore()
|
|
s.global_ignore = ignorefiles.NewGitignore()
|
|
s.respect_ignores = respect_ignores
|
|
if err := s.global_gitignore.LoadLines("*.png", "s/3"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.global_ignore.LoadLines("x/3"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Start()
|
|
for range ch {
|
|
}
|
|
if s.err != nil {
|
|
t.Fatal(s.err)
|
|
}
|
|
ci := CollectionIndex{}
|
|
actual := utils.Map(func(x ResultItem) string { return x.text }, s.Batch(&ci))
|
|
if diff := cmp.Diff(strings.Split(expected, ` `), actual); diff != "" {
|
|
t.Fatalf("Incorrect ignoring:\n%s", diff)
|
|
}
|
|
}
|
|
r(true, `x y b c.png x/s x/1 x/2 y/s y/3 y/4 y/5 x/s/m y/s/6`)
|
|
r(false, `x y a b c.png x/s x/1 x/2 x/3 y/s y/3 y/4 y/5 x/s/m x/s/n y/s/3 y/s/4 y/s/5 y/s/6`)
|
|
}
|
|
|
|
func TestChooseFilesScoring(t *testing.T) {
|
|
root := node{name: string(os.PathSeparator), children: map[string]*node{
|
|
"b": {name: "b"},
|
|
"a": {name: "a"},
|
|
"c.png": {name: "c.png"},
|
|
"x": {name: "x", children: map[string]*node{
|
|
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
|
|
"s": {name: "s", children: map[string]*node{
|
|
"m": {name: "m"}, "n": {name: "n"},
|
|
}},
|
|
}},
|
|
"y": {name: "y", children: map[string]*node{
|
|
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
|
|
}},
|
|
}}
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
s := NewFileSystemScorer(string(os.PathSeparator), "", Filter{}, false, func(err error, is_complete bool) {
|
|
if is_complete {
|
|
wg.Done()
|
|
}
|
|
})
|
|
s.dir_reader = root.ReadDir
|
|
s.global_gitignore = ignorefiles.NewGitignore()
|
|
s.Start()
|
|
wg.Wait()
|
|
results := func() (ans []string) {
|
|
rr, _ := s.Results()
|
|
for _, r := range rr.RenderedMatches(CollectionIndex{}, -1) {
|
|
ans = append(ans, r.text)
|
|
}
|
|
return
|
|
}
|
|
ae := func(query string, expected ...string) {
|
|
wg.Add(1)
|
|
s.Change_query(query)
|
|
wg.Wait()
|
|
if diff := cmp.Diff(expected, results()); diff != "" {
|
|
t.Fatalf("Query less scoring failed\n%s", diff)
|
|
}
|
|
}
|
|
ae("a", "a")
|
|
ae("3", "x/3", "y/3")
|
|
ae("s", "x/s", "x/s/m", "x/s/n")
|
|
ae("sn", "x/s/n")
|
|
ae("", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
|
|
|
|
af := func(filter string, expected ...string) {
|
|
f, _ := NewFilter(filter)
|
|
wg.Add(1)
|
|
s.Change_filter(*f)
|
|
wg.Wait()
|
|
if diff := cmp.Diff(expected, results()); diff != "" {
|
|
t.Fatalf("filter %s failed\n%s", filter, diff)
|
|
}
|
|
}
|
|
af("glob:a:A", "x", "y", "a", "x/s")
|
|
af("glob:[ab]:A", "x", "y", "a", "b", "x/s")
|
|
af("mime:image/png:A", "x", "y", "c.png", "x/s")
|
|
af("mime:image/*:A", "x", "y", "c.png", "x/s")
|
|
af("glob:*:All", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
|
|
}
|
|
|
|
func TestSortedResults(t *testing.T) {
|
|
r := NewSortedResults()
|
|
idx := CollectionIndex{}
|
|
m := func(items ...int) []*ResultItem {
|
|
ans := make([]*ResultItem, len(items))
|
|
for i, x := range items {
|
|
ans[i] = &ResultItem{text: strconv.Itoa(x), score: CombinedScore(x)}
|
|
}
|
|
return ans
|
|
}
|
|
v := func(slice, pos, num int) []int {
|
|
if num == 0 {
|
|
num = r.Len()
|
|
}
|
|
return utils.Map(func(r *ResultItem) int { return int(r.score) }, r.RenderedMatches(CollectionIndex{slice, pos}, num))
|
|
}
|
|
tv := func(slice, pos, num int, expected ...int) {
|
|
if diff := cmp.Diff(expected, v(slice, pos, num)); diff != "" {
|
|
t.Fatalf("view failed for %v num:%d\n%s", CollectionIndex{slice, pos}, num, diff)
|
|
}
|
|
}
|
|
tci := func(increment int, expected int) {
|
|
orig := idx
|
|
idx = r.IncrementIndexWithWrapAround(idx, increment)
|
|
actual := int(r.At(idx).score)
|
|
if actual != expected {
|
|
t.Fatalf("increment: %d on %v failed\nexpected: %d actual: %d idx: %v", increment, orig, expected, actual, idx)
|
|
}
|
|
}
|
|
dt := func(a, b CollectionIndex, expected int) {
|
|
actual := r.distance(a, b)
|
|
if expected != actual {
|
|
t.Fatalf("distance on %v and %v failed\nexpected: %d actual: %d ", a, b, expected, actual)
|
|
}
|
|
if r.distance(b, a) != actual {
|
|
t.Fatalf("distance on %v and %v not commutative %d != %d", a, b, actual, r.distance(b, a))
|
|
}
|
|
}
|
|
tc := func(num_before, expected_new_before int, ci CollectionIndex, expected ...[]int) {
|
|
ac, new_num_before, _ := r.SplitIntoColumns(func(int) int { return 2 }, 2, num_before, ci)
|
|
actual := make([][]int, len(ac))
|
|
for i, x := range ac {
|
|
actual[i] = utils.Map(func(r *ResultItem) int { return int(r.score) }, x)
|
|
}
|
|
if expected_new_before != new_num_before {
|
|
t.Fatalf("new_num_before not as expected for num_before: %d ci: %v\n%d != %d", num_before, ci, expected_new_before, new_num_before)
|
|
}
|
|
if diff := cmp.Diff(expected, actual); diff != "" {
|
|
t.Fatalf("wrong columns for num_before: %d ci: %v\n%s", num_before, ci, diff)
|
|
}
|
|
}
|
|
r.AddSortedSlice(m(10, 20, 30))
|
|
r.AddSortedSlice(m(40, 50, 60))
|
|
r.AddSortedSlice(m(70, 80, 90))
|
|
|
|
tc(0, 0, CollectionIndex{}, []int{10, 20}, []int{30, 40})
|
|
tc(1, 1, CollectionIndex{Pos: 1}, []int{10, 20}, []int{30, 40})
|
|
tc(1, 1, CollectionIndex{Pos: 2}, []int{20, 30}, []int{40, 50})
|
|
tc(2, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
|
|
tc(20, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
|
|
for num_before := range 4 {
|
|
tc(num_before, 3, CollectionIndex{2, 2}, []int{60, 70}, []int{80, 90})
|
|
}
|
|
tc(1, 1, CollectionIndex{1, 1}, []int{40, 50}, []int{60, 70})
|
|
|
|
dt(CollectionIndex{Pos: 0}, CollectionIndex{Pos: 2}, 2)
|
|
dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1}, 3)
|
|
dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1, Pos: 1}, 4)
|
|
|
|
tv(0, 0, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
|
|
tv(0, 2, 3, 30, 40, 50)
|
|
tv(0, 3, 3, 40, 50, 60)
|
|
tv(1, 0, 4, 40, 50, 60, 70)
|
|
|
|
tci(1, 20)
|
|
tci(3, 50)
|
|
tci(-1, 40)
|
|
tci(-3, 10)
|
|
tci(-2, 80)
|
|
tci(3, 20)
|
|
tci(9, 20)
|
|
tci(-9, 20)
|
|
|
|
r.AddSortedSlice(m(100, 110, 120))
|
|
r.AddSortedSlice(m(41, 61, 71, 99))
|
|
tv(0, 0, 0, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
|
|
r.AddSortedSlice(m(1, 2, 3))
|
|
tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
|
|
r.AddSortedSlice(m(1000, 2000))
|
|
tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120, 1000, 2000)
|
|
}
|
|
|
|
func run_scoring(b *testing.B, depth, breadth int, query string) {
|
|
|
|
root := node{name: string(os.PathSeparator)}
|
|
root.generate_random_tree(depth, breadth)
|
|
|
|
for b.Loop() {
|
|
b.StopTimer()
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
s := NewFileSystemScorer(string(os.PathSeparator), query, Filter{}, false, func(err error, is_complete bool) {
|
|
if is_complete {
|
|
wg.Done()
|
|
}
|
|
})
|
|
s.dir_reader = root.ReadDir
|
|
s.global_gitignore = ignorefiles.NewGitignore()
|
|
b.StartTimer()
|
|
s.scanner.Start()
|
|
s.Start()
|
|
wg.Wait()
|
|
}
|
|
fmt.Println("\nnumber of iterations: ", b.N)
|
|
fmt.Println("time per iteration:", b.Elapsed()/time.Duration(b.N))
|
|
}
|
|
|
|
// To run this benchmark with profiling use:
|
|
// go test -bench=FileNameScoringWithoutQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
|
|
func BenchmarkFileNameScoringWithoutQuery(b *testing.B) {
|
|
run_scoring(b, 5, 20, "")
|
|
}
|
|
|
|
// To run this benchmark with profiling use:
|
|
// go test -bench=FileNameScoringWithQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
|
|
func BenchmarkFileNameScoringWithQuery(b *testing.B) {
|
|
run_scoring(b, 5, 20, "abc")
|
|
}
|