all repos

clerk @ 9d057cb160c6849c3dc3c5d0740cc4add015b3f0

missing tooling for ledger/hledger

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

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