all repos

clerk @ 7b8e468208b6ef8bf9db2b5f2f8c1f01b22cb31a

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
feat: config (#6), 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
		t.Run(tt, func(t *testing.T) {
76
			ar := golden.Read(t, tt)
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(b)
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
				TextDocument: protocol.TextDocumentIdentifier{URI: u},
158
				Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},
159
				NewName:      tt.newName,
160
			}
161
			// warm up: first request generates the workspace edits; assert it found one
162
			edit, err := srv.server.Rename(b.Context(), params)
163
			if err != nil {
164
				b.Fatal(err)
165
			}
166
			if edit == nil || len(edit.Changes) == 0 {
167
				b.Fatalf("%s: no edits", tname)
168
			}
169
			b.ReportAllocs()
170
			b.ResetTimer()
171
			for b.Loop() {
172
				if _, err := srv.server.Rename(b.Context(), params); err != nil {
173
					b.Fatal(err)
174
				}
175
			}
176
		})
177
	}
178
}
179
180
// txtarHarness runs per-cursor lsp requests againt golden tests
181
type txtarHarness struct {
182
	srv     *server
183
	uri     uri.URI
184
	content string
185
	cursors []int
186
}
187
188
func newTxtarHarness(t *testing.T, ar *golden.Archive) *txtarHarness {
189
	t.Helper()
190
	dir := t.TempDir()
191
	for _, f := range ar.Files {
192
		if f.Name == "expect" || f.Name == "rename" {
193
			continue
194
		}
195
		if err := os.WriteFile(filepath.Join(dir, f.Name), f.Data, 0o644); err != nil {
196
			t.Fatal(err)
197
		}
198
	}
199
200
	content, cursors := stripCursors(string(ar.Get("in.journal")))
201
	if len(cursors) == 0 {
202
		t.Fatal("no '^' markers in in.journal")
203
	}
204
	u := uri.File(filepath.Join(dir, "in.journal"))
205
206
	srv := newServer(t)
207
	srv.server.openDoc(u, content, 1, "journal")
208
209
	return &txtarHarness{srv: srv.server, uri: u, content: content, cursors: cursors}
210
}
211
212
func (h *txtarHarness) textDocumentPosition(i int) protocol.TextDocumentPositionParams {
213
	return protocol.TextDocumentPositionParams{
214
		TextDocument: protocol.TextDocumentIdentifier{URI: h.uri},
215
		Position:     lsputil.Position(h.content, h.cursors[i]),
216
	}
217
}
218
219
func stripCursors(content string) (string, []int) {
220
	var cursors []int
221
	for {
222
		i := strings.Index(content, "^")
223
		if i < 0 {
224
			return content, cursors
225
		}
226
		cursors = append(cursors, i)
227
		content = content[:i] + content[i+1:]
228
	}
229
}