all repos

clerk @ d12c0794480a4e65333d20e93d8a9f24a53ea056

missing tooling for ledger/hledger

clerk/internal/linter/linter_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
feat: config (#6), 1 month ago
1
package linter
2
3
import (
4
	"encoding/json"
5
	"slices"
6
	"strings"
7
	"testing"
8
9
	"olexsmir.xyz/clerk/internal/analyzer"
10
	"olexsmir.xyz/clerk/internal/testutil/golden"
11
	"olexsmir.xyz/clerk/journal"
12
)
13
14
var tests = map[string][]Rule{
15
	"correct":                  allRules(),
16
	"invalid-include":          {&InvalidInclude{}},
17
	"empty-postings":           {&EmptyPostings{}},
18
	"parse-error":              {&ParseError{}},
19
	"omitted-precision":        {&OmittedPrecision{}},
20
	"missing-commodity":        {&MissingCommodity{}},
21
	"missing-status":           {&MissingStatus{}},
22
	"missing-payee":            {&MissingPayee{}},
23
	"account-depth":            {&AccountDepthLimit{MaxDepth: 3}},
24
	"multiple-omitted-amounts": {&MultipleOmittedAmounts{}},
25
	"orderdate":                {&OrderDate{}},
26
	"duplicated-account":       {&DuplicatedAccount{}},
27
	"duplicated-commodity":     {&DuplicatedCommodity{}},
28
	"duplicated-transaction":   {&DuplicatedTransaction{}},
29
	"duplicated-tag":           {&DuplicatedTag{}},
30
	"undeclared-tag":           {&UndeclaredTag{}},
31
	"undeclared-commodity":     {&UndeclaredCommodity{}},
32
	"undeclared-account":       {&UndeclaredAccount{}},
33
	"undeclared-payee":         {&UndeclaredPayee{}},
34
	"unbalanced-transaction":   {&UnbalancedTransaction{}},
35
	"unused-account":           {&UnusedAccount{}},
36
	"unused-tag":               {&UnusedTag{}},
37
	"invalid-date-tag":         {&InvalidDateTag{}},
38
	"invalid-type-tag":         {&InvalidTypeTag{}},
39
}
40
41
func TestLinter(t *testing.T) {
42
	for tname, trules := range tests {
43
		t.Run(tname, func(t *testing.T) {
44
			a := golden.Read(t, tname)
45
			fsys, err := a.FS()
46
			if err != nil {
47
				t.Fatal(err)
48
			}
49
50
			l := journal.NewLoader()
51
			rj, err := l.ResolveFS(fsys, "in.journal")
52
			if err != nil {
53
				t.Fatalf("failed to load test journal: %v", err)
54
			}
55
56
			ctx := analyzer.Build(rj)
57
			finds := (&Linter{rules: trules}).Run(ctx)
58
59
			var b strings.Builder
60
			fprint(&b, PathBasename, finds)
61
			golden.Assert(t, a, b.String())
62
		})
63
	}
64
}
65
66
func BenchmarkLinter(b *testing.B) {
67
	ldr := journal.NewLoader()
68
	rj, err := ldr.Resolve("../../journal/testdata/journals/actual-1ktxns-100accts.journal")
69
	if err != nil {
70
		b.Fatalf("failed to load benchmark journal: %v", err)
71
	}
72
73
	ctx := analyzer.Build(rj)
74
	l, err := NewLinter(Config{})
75
	if err != nil {
76
		b.Fatal(err)
77
	}
78
79
	b.ResetTimer()
80
	b.ReportAllocs()
81
	for b.Loop() {
82
		l.Run(ctx)
83
	}
84
}
85
86
func allRules() []Rule {
87
	out := make([]Rule, 0, len(Rules))
88
	for _, b := range Rules {
89
		out = append(out, b.Rule)
90
	}
91
	return out
92
}
93
94
func TestNewLinter(t *testing.T) {
95
	offByDefault := []RuleID{UndeclaredPayeeID, UndeclaredTagID}
96
	t.Run("all-default", func(t *testing.T) {
97
		l, err := NewLinter(Config{})
98
		if err != nil {
99
			t.Fatal(err)
100
		}
101
		var got []RuleID
102
		for _, r := range l.rules {
103
			got = append(got, r.ID())
104
		}
105
		if want := without(builtinIDs(), offByDefault...); !slices.Equal(got, want) {
106
			t.Errorf("got %v, want %v", got, want)
107
		}
108
	})
109
110
	t.Run("disabled-omitted", func(t *testing.T) {
111
		l, err := NewLinter(Config{Rules: map[RuleID]RuleConfig{
112
			OrderDateID:    {Disabled: true},
113
			MissingPayeeID: {Disabled: true},
114
		}})
115
		if err != nil {
116
			t.Fatal(err)
117
		}
118
		var got []RuleID
119
		for _, r := range l.rules {
120
			got = append(got, r.ID())
121
		}
122
		want := without(builtinIDs(), OrderDateID, MissingPayeeID)
123
		want = without(want, offByDefault...)
124
		if !slices.Equal(got, want) {
125
			t.Errorf("got %v, want %v", got, want)
126
		}
127
	})
128
129
	t.Run("severity-none-enabled-by-config", func(t *testing.T) {
130
		l, err := NewLinter(Config{Rules: map[RuleID]RuleConfig{UndeclaredPayeeID: {Severity: SeverityWarning}}})
131
		if err != nil {
132
			t.Fatal(err)
133
		}
134
		var got []RuleID
135
		for _, r := range l.rules {
136
			got = append(got, r.ID())
137
		}
138
		want := without(builtinIDs(), UndeclaredTagID)
139
		if !slices.Equal(got, want) {
140
			t.Errorf("got %v, want %v", got, want)
141
		}
142
	})
143
144
	t.Run("options-applied", func(t *testing.T) {
145
		l, err := NewLinter(Config{Rules: map[RuleID]RuleConfig{
146
			AccountDepthLimitID: {Options: json.RawMessage(`{"max-depth": 2}`)},
147
		}})
148
		if err != nil {
149
			t.Fatal(err)
150
		}
151
		for _, r := range l.rules {
152
			if r.ID() != AccountDepthLimitID {
153
				continue
154
			}
155
			if got := r.(*AccountDepthLimit).MaxDepth; got != 2 {
156
				t.Errorf("MaxDepth = %d, want 2", got)
157
			}
158
			if def := Rules[AccountDepthLimitID].Rule.(*AccountDepthLimit).MaxDepth; def != 4 {
159
				t.Errorf("shared built-in mutated: MaxDepth = %d, want 4", def)
160
			}
161
			return
162
		}
163
		t.Error("account-depth rule not found")
164
	})
165
166
	t.Run("options-on-non-optionable-rule", func(t *testing.T) {
167
		cfg := Config{Rules: map[RuleID]RuleConfig{
168
			ParseErrorID: {Options: json.RawMessage(`{"x":1}`)},
169
		}}
170
		if _, err := NewLinter(cfg); err == nil {
171
			t.Error("expected error")
172
		}
173
	})
174
175
	t.Run("unknown-option-key", func(t *testing.T) {
176
		cfg := Config{Rules: map[RuleID]RuleConfig{
177
			AccountDepthLimitID: {Options: json.RawMessage(`{"nope": 1}`)},
178
		}}
179
		if _, err := NewLinter(cfg); err == nil {
180
			t.Error("expected error")
181
		}
182
	})
183
}
184
185
func builtinIDs() []RuleID {
186
	ids := make([]RuleID, 0, len(Rules))
187
	for id := range Rules {
188
		ids = append(ids, id)
189
	}
190
	slices.Sort(ids)
191
	return ids
192
}
193
194
func without(ids []RuleID, drop ...RuleID) []RuleID {
195
	out := ids[:0:0]
196
	for _, id := range ids {
197
		if !slices.Contains(drop, id) {
198
			out = append(out, id)
199
		}
200
	}
201
	return out
202
}