all repos

clerk @ b457e3f

missing tooling for ledger/hledger

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

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