all repos

clerk @ 97e9bd7

missing tooling for ledger/hledger

clerk/internal/lsp/textdocument_hover_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
	"context"
5
	"fmt"
6
	"os"
7
	"strings"
8
	"testing"
9
	"time"
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
	"olexsmir.xyz/clerk/journal/ast"
19
	"olexsmir.xyz/clerk/journal/token"
20
)
21
22
func TestTagValueSpan(t *testing.T) {
23
	tag := func(start, end int) *ast.Tag {
24
		return &ast.Tag{
25
			Span: token.Span{Start: token.Pos{Offset: start}, End: token.Pos{Offset: end}},
26
		}
27
	}
28
29
	tests := map[string]struct {
30
		content            string
31
		tag                *ast.Tag
32
		wantStart, wantEnd int
33
	}{
34
		"value":                {"; project:home", tag(2, 14), 10, 14},
35
		"empty value":          {"; done:", tag(2, 7), 7, 7},
36
		"trailing space":       {"; done: ", tag(2, 8), 7, 7},
37
		"second of comma list": {"; a:1, b:2", tag(7, 10), 9, 10},
38
	}
39
	for tname, tt := range tests {
40
		t.Run(tname, func(t *testing.T) {
41
			span := tagValueSpan(tt.content, tt.tag)
42
			if span.Start.Offset != tt.wantStart || span.End.Offset != tt.wantEnd {
43
				t.Errorf("got %d-%d, want %d-%d", span.Start.Offset, span.End.Offset, tt.wantStart, tt.wantEnd)
44
			}
45
		})
46
	}
47
}
48
49
func TestCountTransactionsOnDate(t *testing.T) {
50
	tx := func(y, m, d int) *ast.Transaction { return &ast.Transaction{Date: ast.Date{Year: y, Month: m, Day: d}} }
51
	an := &analyzer.Analysis{Transactions: []*ast.Transaction{tx(2024, 1, 15), tx(2024, 1, 15), tx(2024, 2, 1)}}
52
	if got := countTransactionsOnDate(an, ast.Date{Year: 2024, Month: 1, Day: 15}); got != 2 {
53
		t.Errorf("got %d, want 2", got)
54
	}
55
	if got := countTransactionsOnDate(an, ast.Date{Year: 2025, Month: 1, Day: 15}); got != 0 {
56
		t.Errorf("got %d, want 0", got)
57
	}
58
}
59
60
func TestServerHover_DocumentNotFound(t *testing.T) {
61
	srv := NewServer("test")
62
	res, err := srv.server.Hover(context.Background(), &protocol.HoverParams{
63
		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
64
			TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///nonexistent.journal")},
65
			Position:     protocol.Position{Line: 0, Character: 0},
66
		},
67
	})
68
	if err != nil {
69
		t.Fatal(err)
70
	}
71
	if res != nil {
72
		t.Errorf("got %v, want nil", res)
73
	}
74
}
75
76
// Golden
77
78
func TestGolden_Hover(t *testing.T) {
79
	for _, tt := range []string{"hover-journal", "hover-include"} {
80
		ar := golden.Read(t, tt)
81
		t.Run(tt, func(t *testing.T) {
82
			h := newTxtarHarness(t, ar)
83
84
			var b strings.Builder
85
			for i := range h.cursors {
86
				tdp := h.textDocumentPosition(i)
87
				res, err := h.srv.Hover(t.Context(), &protocol.HoverParams{
88
					TextDocumentPositionParams: tdp,
89
				})
90
				if err != nil {
91
					t.Fatal(err)
92
				}
93
				if res == nil {
94
					fmt.Fprintf(&b, "%d:%d <none>\n", tdp.Position.Line, tdp.Position.Character)
95
					continue
96
				}
97
				mc, ok := res.Contents.(*protocol.MarkupContent)
98
				if !ok {
99
					t.Fatalf("Hover returned %T, want *MarkupContent", res.Contents)
100
				}
101
				fmt.Fprintf(&b, "%d:%d %q %d:%d-%d:%d\n",
102
					tdp.Position.Line, tdp.Position.Character, mc.Value,
103
					res.Range.Start.Line, res.Range.Start.Character, res.Range.End.Line, res.Range.End.Character)
104
			}
105
			golden.Assert(t, ar, b.String())
106
		})
107
	}
108
}
109
110
func BenchmarkHover(b *testing.B) {
111
	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"
112
	src, err := os.ReadFile(path)
113
	if err != nil {
114
		b.Fatal(err)
115
	}
116
	content := string(src)
117
118
	// Path must match the open doc: hoverAt resolves the document by path.
119
	an := analyzer.Build(journal.NewLoader().ResolveBytes("/test.journal", src))
120
121
	srv := NewServer("test")
122
	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")
123
	srv.server.current = an
124
125
	for tname, tt := range map[string]int{
126
		"date":              strings.Index(content, "2000-01-01 transaction 1") + 3,
127
		"account":           strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2,
128
		"payee":             strings.Index(content, "transaction 1") + len("transaction ") + 1,
129
		"amount with cost":  strings.Index(content, "2 B @@") + 2,
130
		"commodity in cost": strings.Index(content, "@@ 2 C") + len("@@ 2 C") - 1,
131
		"late amount":       strings.Index(content, "1000 L @") + 2,
132
	} {
133
		b.Run(tname, func(b *testing.B) {
134
			line, col := lsputil.LineCol(content, tt)
135
			params := &protocol.HoverParams{
136
				TextDocumentPositionParams: protocol.TextDocumentPositionParams{
137
					TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},
138
					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},
139
				},
140
			}
141
142
			// warm up
143
			if _, err := srv.server.Hover(b.Context(), params); err != nil {
144
				b.Fatal(err)
145
			}
146
			b.ReportAllocs()
147
			b.ResetTimer()
148
			for b.Loop() {
149
				if _, err := srv.server.Hover(b.Context(), params); err != nil {
150
					b.Fatal(err)
151
				}
152
			}
153
154
			// guard: hover walks entries from the top of the file, so a cursor
155
			// near the end costs ~0.8ms on this file; a whole-file re-parse per
156
			// request (~13ms) would blow past this and must be caught
157
			if avg := b.Elapsed() / time.Duration(b.N); avg > 5*time.Millisecond {
158
				b.Fatalf("hover %v/op: entry walk or reparse regression", avg)
159
			}
160
		})
161
	}
162
}