all repos

clerk @ 25c8f80b71674079b26e4189b4030a90fffb8c26

missing tooling for ledger/hledger

clerk/internal/lsp/workspace_symbol_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: comply with lsp3.18 lifecycle, 29 days ago
1
package lsp
2
3
import (
4
	"fmt"
5
	"path/filepath"
6
	"strings"
7
	"testing"
8
	"time"
9
10
	"go.lsp.dev/protocol"
11
	"go.lsp.dev/uri"
12
13
	"olexsmir.xyz/clerk/internal/testutil/golden"
14
)
15
16
func TestSortScoredSymbols(t *testing.T) {
17
	scored := []scoredSymbol{
18
		{symbolAccount, "zz:zz", 0.5, nil},
19
		{symbolTag, "aa", 1.0, nil},
20
		{symbolPayee, "bb", 0.5, nil},
21
		{symbolTag, "ccc", 0.5, nil},
22
		{symbolPayee, "bb", 0.5, nil},
23
	}
24
	sortScoredSymbols(scored)
25
26
	want := []string{"aa", "bb", "bb", "ccc", "zz:zz"}
27
	for i, s := range scored {
28
		if s.name != want[i] {
29
			t.Errorf("pos %d = %q, want %q", i, s.name, want[i])
30
		}
31
	}
32
}
33
34
func TestServer_Symbols_EmptyQuery(t *testing.T) {
35
	srv := newServer(t)
36
	u := uri.URI("file:///test.journal")
37
	srv.server.openDoc(u, "account expenses:food\n", 1, "journal")
38
39
	res, err := srv.server.Symbols(t.Context(), &protocol.WorkspaceSymbolParams{Query: ""})
40
	if err != nil {
41
		t.Fatal(err)
42
	}
43
	if res != nil {
44
		t.Errorf("got %v, want nil for empty query", res)
45
	}
46
}
47
48
func TestServer_Symbols_SearchAllOpenDocs(t *testing.T) {
49
	srv := newServer(t)
50
	srv.server.openDoc(uri.URI("file:///a.journal"), "account assets:only\n", 1, "journal")
51
	srv.server.openDoc(uri.URI("file:///b.journal"), "2024-01-15 Grocery Store\n    expenses:food  $50\n    assets:cash  $-50\n", 1, "journal")
52
53
	res, err := srv.server.Symbols(t.Context(), &protocol.WorkspaceSymbolParams{Query: "2024"})
54
	if err != nil {
55
		t.Fatal(err)
56
	}
57
	list, ok := res.(protocol.WorkspaceSymbolSlice)
58
	if !ok {
59
		t.Fatalf("Symbols returned %T, want WorkspaceSymbolSlice", res)
60
	}
61
	if len(list) != 1 || list[0].Name != "2024-01-15 Grocery Store" {
62
		t.Errorf("got %v, want the transaction from the second doc", list)
63
	}
64
}
65
66
func TestGolden_Symbols(t *testing.T) {
67
	ar := golden.Read(t, "workspace-symbol")
68
	h := newTxtarHarness(t, ar)
69
70
	// One query per line; a trailing blank line is the empty-query case.
71
	queries := strings.Split(strings.TrimSuffix(string(ar.Get("queries")), "\n"), "\n")
72
	var b strings.Builder
73
	for _, q := range queries {
74
		res, err := h.srv.Symbols(t.Context(), &protocol.WorkspaceSymbolParams{Query: q})
75
		if err != nil {
76
			t.Fatal(err)
77
		}
78
		if res == nil {
79
			fmt.Fprintf(&b, "%q <none>\n", q)
80
			continue
81
		}
82
		list, ok := res.(protocol.WorkspaceSymbolSlice)
83
		if !ok {
84
			t.Fatalf("Symbols returned %T, want WorkspaceSymbolSlice", res)
85
		}
86
		for _, sym := range list {
87
			loc, ok := sym.Location.(*protocol.Location)
88
			if !ok {
89
				t.Fatalf("Symbol %q location is %T, want *Location", sym.Name, sym.Location)
90
			}
91
			r := loc.Range
92
			fmt.Fprintf(&b, "%q %s %s %s %d:%d-%d:%d\n", q, symbolKindName(sym.Kind), sym.Name,
93
				filepath.Base(loc.URI.Path()),
94
				r.Start.Line, r.Start.Character, r.End.Line, r.End.Character)
95
		}
96
	}
97
	golden.Assert(t, ar, b.String())
98
}
99
100
func BenchmarkSymbols(b *testing.B) {
101
	content := openJournal(b, "../../journal/testdata/journals/actual-1ktxns-100accts.journal")
102
103
	srv := newServer(b)
104
	u := uri.URI("file:///test.journal")
105
	srv.server.openDoc(u, content, 1, "journal")
106
	srv.server.analysisFor(u) // warm the per-doc cache
107
108
	for tname, tt := range map[string]struct {
109
		query string
110
		want  bool // expect non-empty results
111
	}{
112
		"account prefix": {"1:2", true},
113
		"all payees":     {"transaction", true},
114
		"txn dates":      {"2000", true},
115
		"mixed kinds":    {"B", true},
116
		"no match":       {"xyz", false},
117
	} {
118
		b.Run(tname, func(b *testing.B) {
119
			params := &protocol.WorkspaceSymbolParams{Query: tt.query}
120
121
			// warm up: assert the query matches as expected
122
			res, err := srv.server.Symbols(b.Context(), params)
123
			if err != nil {
124
				b.Fatal(err)
125
			}
126
			if (res != nil) != tt.want {
127
				b.Fatalf("%s: query %q: got res==nil=%v, want %v", tname, tt.query, res == nil, !tt.want)
128
			}
129
			b.ReportAllocs()
130
			b.ResetTimer()
131
			for b.Loop() {
132
				if _, err := srv.server.Symbols(b.Context(), params); err != nil {
133
					b.Fatal(err)
134
				}
135
			}
136
137
			// guard: each request deliberately rebuilds the workspace analysis
138
			// (~7ms of [analyzer.Build] per 1k transactions); a cost beyond that
139
			// (e.g. a lost loader parse cache) must be caught
140
			if avg := b.Elapsed() / time.Duration(b.N); avg > 20*time.Millisecond {
141
				b.Fatalf("symbols %v/op: regression beyond the per-request rebuild", avg)
142
			}
143
		})
144
	}
145
}
146
147
func symbolKindName(k protocol.SymbolKind) string {
148
	switch k {
149
	case protocol.SymbolKindClass:
150
		return "class"
151
	case protocol.SymbolKindVariable:
152
		return "variable"
153
	case protocol.SymbolKindObject:
154
		return "object"
155
	case protocol.SymbolKindProperty:
156
		return "property"
157
	case protocol.SymbolKindEvent:
158
		return "event"
159
	}
160
	return "other"
161
}