all repos

clerk @ 5581d44cd1b540070af709dfb8aafc68722f1195

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
tests: consistent golden.Read placement, 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
	h := newTxtarHarness(t, ar)
70
71
	// One query per line; a trailing blank line is the empty-query case.
72
	queries := strings.Split(strings.TrimSuffix(string(ar.Get("queries")), "\n"), "\n")
73
	var b strings.Builder
74
	for _, q := range queries {
75
		res, err := h.srv.Symbols(t.Context(), &protocol.WorkspaceSymbolParams{Query: q})
76
		if err != nil {
77
			t.Fatal(err)
78
		}
79
		if res == nil {
80
			fmt.Fprintf(&b, "%q <none>\n", q)
81
			continue
82
		}
83
		list, ok := res.(protocol.WorkspaceSymbolSlice)
84
		if !ok {
85
			t.Fatalf("Symbols returned %T, want WorkspaceSymbolSlice", res)
86
		}
87
		for _, sym := range list {
88
			loc, ok := sym.Location.(*protocol.Location)
89
			if !ok {
90
				t.Fatalf("Symbol %q location is %T, want *Location", sym.Name, sym.Location)
91
			}
92
			r := loc.Range
93
			fmt.Fprintf(&b, "%q %s %s %s %d:%d-%d:%d\n", q, symbolKindName(sym.Kind), sym.Name,
94
				filepath.Base(loc.URI.Path()),
95
				r.Start.Line, r.Start.Character, r.End.Line, r.End.Character)
96
		}
97
	}
98
	golden.Assert(t, ar, b.String())
99
}
100
101
func BenchmarkSymbols(b *testing.B) {
102
	content := openJournal(b, "../../journal/testdata/journals/actual-1ktxns-100accts.journal")
103
104
	srv := NewServer("test")
105
	u := uri.URI("file:///test.journal")
106
	srv.server.openDoc(u, content, 1, "journal")
107
	srv.server.analysisFor(u) // warm the per-doc cache
108
109
	for tname, tt := range map[string]struct {
110
		query string
111
		want  bool // expect non-empty results
112
	}{
113
		"account prefix": {"1:2", true},
114
		"all payees":     {"transaction", true},
115
		"txn dates":      {"2000", true},
116
		"mixed kinds":    {"B", true},
117
		"no match":       {"xyz", false},
118
	} {
119
		b.Run(tname, func(b *testing.B) {
120
			params := &protocol.WorkspaceSymbolParams{Query: tt.query}
121
122
			// warm up: assert the query matches as expected
123
			res, err := srv.server.Symbols(b.Context(), params)
124
			if err != nil {
125
				b.Fatal(err)
126
			}
127
			if (res != nil) != tt.want {
128
				b.Fatalf("%s: query %q: got res==nil=%v, want %v", tname, tt.query, res == nil, !tt.want)
129
			}
130
			b.ReportAllocs()
131
			b.ResetTimer()
132
			for b.Loop() {
133
				if _, err := srv.server.Symbols(b.Context(), params); err != nil {
134
					b.Fatal(err)
135
				}
136
			}
137
138
			// guard: each request deliberately rebuilds the workspace analysis
139
			// (~7ms of [analyzer.Build] per 1k transactions); a cost beyond that
140
			// (e.g. a lost loader parse cache) must be caught
141
			if avg := b.Elapsed() / time.Duration(b.N); avg > 20*time.Millisecond {
142
				b.Fatalf("symbols %v/op: regression beyond the per-request rebuild", avg)
143
			}
144
		})
145
	}
146
}
147
148
func symbolKindName(k protocol.SymbolKind) string {
149
	switch k {
150
	case protocol.SymbolKindClass:
151
		return "class"
152
	case protocol.SymbolKindVariable:
153
		return "variable"
154
	case protocol.SymbolKindObject:
155
		return "object"
156
	case protocol.SymbolKindProperty:
157
		return "property"
158
	case protocol.SymbolKindEvent:
159
		return "event"
160
	}
161
	return "other"
162
}