all repos

clerk @ 33b67bd00c3ee9541f6680de8ac384cc156fb3a6

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: add bechmarks, consisten golden tests naming, 1 month ago
1
package lsp
2
3
import (
4
	"fmt"
5
	"os"
6
	"path/filepath"
7
	"sort"
8
	"strings"
9
	"testing"
10
11
	"go.lsp.dev/protocol"
12
	"go.lsp.dev/uri"
13
14
	"olexsmir.xyz/clerk/internal/analyzer"
15
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
16
	"olexsmir.xyz/clerk/internal/testutil/golden"
17
	"olexsmir.xyz/clerk/journal"
18
)
19
20
func TestValidateRenameName(t *testing.T) {
21
	valid := map[string][]string{
22
		"account":   {"expenses", "expenses:food", "a:b:c"},
23
		"commodity": {"$", "€", "USD", "US$"},
24
		"payee":     {"Grocery Store", "acme", "продукти"},
25
		"tag":       {"client", "a-b_c", "x123"},
26
	}
27
28
	invalid := map[string][]string{
29
		"account":   {"", " expenses", "expenses ", "expenses;x", "expenses\nx", "expenses\r"},
30
		"commodity": {"", " $", "$ ", "$;x", "$\n"},
31
		"payee":     {"", " payee", "payee|note", "payee;x"},
32
		"tag":       {"", "a:b", "a,b", "a b", "a;b", "tag\tx"},
33
	}
34
35
	validators := map[string]func(string) error{
36
		"account":   validateAccountName,
37
		"commodity": validateCommodityName,
38
		"payee":     validatePayeeName,
39
		"tag":       validateTagName,
40
	}
41
42
	for kind, v := range validators {
43
		for _, name := range valid[kind] {
44
			if err := v(name); err != nil {
45
				t.Errorf("%s %q: unexpected error %v", kind, name, err)
46
			}
47
		}
48
		for _, name := range invalid[kind] {
49
			if err := v(name); err == nil {
50
				t.Errorf("%s %q: expected error", kind, name)
51
			}
52
		}
53
	}
54
}
55
56
func TestAccountMatches(t *testing.T) {
57
	tests := map[string]bool{
58
		"expenses":        true,
59
		"expenses:food":   true,
60
		"expenses:food:l": true,
61
		"expenses2":       false,
62
		"ex":              false,
63
		"other":           false,
64
	}
65
	for name, want := range tests {
66
		if got := accountMatches(name, "expenses"); got != want {
67
			t.Errorf("accountMatches(%q, expenses) = %v, want %v", name, got, want)
68
		}
69
	}
70
}
71
72
// Golden
73
74
func TestGolden_Rename(t *testing.T) {
75
	for _, tt := range []string{"rename-account", "rename-commodity", "rename-payee", "rename-tag"} {
76
		ar := golden.Read(t, tt)
77
		t.Run(tt, func(t *testing.T) {
78
			h := newTxtarHarness(t, ar)
79
80
			newNames := strings.Split(strings.TrimRight(string(ar.Get("rename")), "\n"), "\n")
81
			if len(newNames) != len(h.cursors) {
82
				t.Fatalf("%d cursors, %d rename names", len(h.cursors), len(newNames))
83
			}
84
85
			var b strings.Builder
86
			for i := range h.cursors {
87
				tdp := h.textDocumentPosition(i)
88
				prep, err := h.srv.PrepareRename(t.Context(), &protocol.PrepareRenameParams{TextDocumentPositionParams: tdp})
89
				if err != nil {
90
					t.Fatal(err)
91
				}
92
				edit, err := h.srv.Rename(t.Context(), &protocol.RenameParams{
93
					TextDocumentPositionParams: tdp,
94
					NewName:                    newNames[i],
95
				})
96
				if err != nil {
97
					t.Fatal(err)
98
				}
99
100
				fmt.Fprintf(&b, "%d:%d\n", tdp.Position.Line, tdp.Position.Character)
101
				if p, ok := prep.(*protocol.PrepareRenamePlaceholder); ok {
102
					r := p.Range
103
					fmt.Fprintf(&b, "  prep in.journal %d:%d-%d:%d %q\n",
104
						r.Start.Line, r.Start.Character, r.End.Line, r.End.Character, p.Placeholder)
105
				} else {
106
					b.WriteString("  prep <none>\n")
107
				}
108
				if edit == nil {
109
					b.WriteString("  ren <none>\n")
110
					continue
111
				}
112
				uris := make([]string, 0, len(edit.Changes))
113
				for u := range edit.Changes {
114
					uris = append(uris, string(u))
115
				}
116
				sort.Strings(uris)
117
				for _, us := range uris {
118
					for _, e := range edit.Changes[uri.URI(us)] {
119
						r := e.Range
120
						fmt.Fprintf(&b, "  ren %s %d:%d-%d:%d %q\n", filepath.Base(uri.URI(us).Path()),
121
							r.Start.Line, r.Start.Character, r.End.Line, r.End.Character, e.NewText)
122
					}
123
				}
124
			}
125
			golden.Assert(t, ar, b.String())
126
		})
127
	}
128
}
129
130
func BenchmarkRename(b *testing.B) {
131
	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"
132
	abs, err := filepath.Abs(path)
133
	if err != nil {
134
		b.Fatal(err)
135
	}
136
	rj, err := journal.NewLoader().Resolve(abs)
137
	if err != nil {
138
		b.Fatal(err)
139
	}
140
	a := analyzer.Build(rj)
141
	content := string(rj.Occurrences[0].Src)
142
143
	srv := NewServer("test")
144
	u := uri.File(abs)
145
	srv.server.openDoc(u, content, 1, "journal")
146
	srv.server.current = a
147
148
	for tname, tt := range map[string]struct {
149
		newName string
150
		pos     int
151
	}{
152
		"1k txns, account":   {"1:2:3x", strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2},
153
		"1k txns, payee":     {"transaction 1x", strings.Index(content, "transaction 1") + len("transaction ")},
154
		"1k txns, commodity": {"Bx", strings.Index(content, "2 B @@") + len("2 B")},
155
	} {
156
		b.Run(tname, func(b *testing.B) {
157
			line, col := lsputil.LineCol(content, tt.pos)
158
			params := &protocol.RenameParams{
159
				TextDocumentPositionParams: protocol.TextDocumentPositionParams{
160
					TextDocument: protocol.TextDocumentIdentifier{URI: u},
161
					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},
162
				},
163
				NewName: tt.newName,
164
			}
165
			// warm up: first request generates the workspace edits; assert it found one
166
			edit, err := srv.server.Rename(b.Context(), params)
167
			if err != nil {
168
				b.Fatal(err)
169
			}
170
			if edit == nil || len(edit.Changes) == 0 {
171
				b.Fatalf("%s: no edits", tname)
172
			}
173
			b.ReportAllocs()
174
			b.ResetTimer()
175
			for b.Loop() {
176
				if _, err := srv.server.Rename(b.Context(), params); err != nil {
177
					b.Fatal(err)
178
				}
179
			}
180
		})
181
	}
182
}
183
184
// txtarHarness runs per-cursor lsp requests againt golden tests
185
type txtarHarness struct {
186
	srv     *server
187
	uri     uri.URI
188
	content string
189
	cursors []int
190
}
191
192
func newTxtarHarness(t *testing.T, ar *golden.Archive) *txtarHarness {
193
	t.Helper()
194
	dir := t.TempDir()
195
	for _, f := range ar.Files {
196
		if f.Name == "expect" || f.Name == "rename" {
197
			continue
198
		}
199
		if err := os.WriteFile(filepath.Join(dir, f.Name), f.Data, 0o644); err != nil {
200
			t.Fatal(err)
201
		}
202
	}
203
204
	content, cursors := stripCursors(string(ar.Get("in.journal")))
205
	if len(cursors) == 0 {
206
		t.Fatal("no '^' markers in in.journal")
207
	}
208
	u := uri.File(filepath.Join(dir, "in.journal"))
209
210
	srv := NewServer("test")
211
	srv.server.openDoc(u, content, 1, "journal")
212
213
	return &txtarHarness{srv: srv.server, uri: u, content: content, cursors: cursors}
214
}
215
216
func (h *txtarHarness) textDocumentPosition(i int) protocol.TextDocumentPositionParams {
217
	return protocol.TextDocumentPositionParams{
218
		TextDocument: protocol.TextDocumentIdentifier{URI: h.uri},
219
		Position:     lsputil.Position(h.content, h.cursors[i]),
220
	}
221
}
222
223
func stripCursors(content string) (string, []int) {
224
	var cursors []int
225
	for {
226
		i := strings.Index(content, "^")
227
		if i < 0 {
228
			return content, cursors
229
		}
230
		cursors = append(cursors, i)
231
		content = content[:i] + content[i+1:]
232
	}
233
}