clerk/internal/settings/settings_test.go (view raw)
| 1 | package settings |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "slices" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "olexsmir.xyz/clerk/internal/linter" |
| 12 | "olexsmir.xyz/clerk/internal/testutil/golden" |
| 13 | "olexsmir.xyz/clerk/journal/printer" |
| 14 | ) |
| 15 | |
| 16 | func TestNormKey(t *testing.T) { |
| 17 | cases := map[string]string{ |
| 18 | "semantic_highlighting": "semantic_highlighting", |
| 19 | "semantic-highlighting": "semantic_highlighting", |
| 20 | "semanticHighlighting": "semantic_highlighting", |
| 21 | "latin_to_cyrillic_completion": "latin_to_cyrillic_completion", |
| 22 | "latinToCyrillicCompletion": "latin_to_cyrillic_completion", |
| 23 | "format": "format", |
| 24 | "ALIGN_STYLE": "align_style", |
| 25 | "indent-width": "indent_width", |
| 26 | } |
| 27 | for in, want := range cases { |
| 28 | if got := normalizeKey(in); got != want { |
| 29 | t.Errorf("normKey(%q) = %q, want %q", in, got, want) |
| 30 | } |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | func TestApplyLSP(t *testing.T) { |
| 35 | s := DefaultConfig |
| 36 | _, err := s.ApplyLSP(map[string]any{ |
| 37 | "semanticHighlighting": false, |
| 38 | "latinToCyrillicCompletion": true, |
| 39 | "format": map[string]any{"indent-width": int64(4)}, |
| 40 | }) |
| 41 | if err != nil { |
| 42 | t.Fatalf("ApplyLSP: %v", err) |
| 43 | } |
| 44 | if s.SemanticHighlighting { |
| 45 | t.Error("SemanticHighlighting = true, want false") |
| 46 | } |
| 47 | if !s.LatinToCyrillicCompletion { |
| 48 | t.Error("LatinToCyrillicCompletion = false, want true") |
| 49 | } |
| 50 | if s.Format.IndentWidth != 4 { |
| 51 | t.Errorf("IndentWidth = %d, want 4", s.Format.IndentWidth) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func TestApply_duplicateKey(t *testing.T) { |
| 56 | s := DefaultConfig |
| 57 | _, err := s.Apply(map[string]any{ |
| 58 | "format": map[string]any{ |
| 59 | "indent-width": 4, |
| 60 | "indent_width": 8, |
| 61 | }, |
| 62 | }) |
| 63 | if err == nil { |
| 64 | t.Fatal("expected duplicate-key error, got nil") |
| 65 | } |
| 66 | // Neither conflicting value is applied; default prevails. |
| 67 | if s.Format.IndentWidth != DefaultConfig.Format.IndentWidth { |
| 68 | t.Errorf("IndentWidth = %d, want default %d", s.Format.IndentWidth, DefaultConfig.Format.IndentWidth) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // Benchmarks |
| 73 | |
| 74 | func BenchmarkParseMap(b *testing.B) { |
| 75 | benchRawMap := map[string]any{ |
| 76 | "semantic_highlighting": true, |
| 77 | "format": map[string]any{ |
| 78 | "tab-indent": true, |
| 79 | "indent-width": int64(4), |
| 80 | "align-style": "right", |
| 81 | "commodity-pos": "before", |
| 82 | "align-column": int64(80), |
| 83 | "preserve-blank-lines": true, |
| 84 | }, |
| 85 | "lint": map[string]any{ |
| 86 | "unbalanced-transaction": "error", |
| 87 | "missing-payee": false, |
| 88 | "empty-postings": "warn", |
| 89 | "account-depth": map[string]any{"severity": "error", "max-depth": int64(8)}, |
| 90 | }, |
| 91 | } |
| 92 | |
| 93 | b.ReportAllocs() |
| 94 | for b.Loop() { |
| 95 | if _, _, err := parse(benchRawMap); err != nil { |
| 96 | b.Fatal(err) |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func BenchmarkParseLargeLint(b *testing.B) { |
| 102 | raw := map[string]any{"lint": map[string]any{}} |
| 103 | for id := range linter.Rules { |
| 104 | raw["lint"].(map[string]any)[string(id)] = "error" |
| 105 | } |
| 106 | b.ReportAllocs() |
| 107 | for b.Loop() { |
| 108 | if _, _, err := parse(raw); err != nil { |
| 109 | b.Fatal(err) |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func BenchmarkLoad(b *testing.B) { |
| 115 | path := filepath.Join("testdata", "bench.toml") |
| 116 | b.ReportAllocs() |
| 117 | for b.Loop() { |
| 118 | if _, _, err := Load(path); err != nil { |
| 119 | b.Fatal(err) |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func BenchmarkNormKey(b *testing.B) { |
| 125 | tests := []string{"latinToCyrillicCompletion", "semantic_highlighting", "unbalanced-transaction"} |
| 126 | for _, test := range tests { |
| 127 | b.ReportAllocs() |
| 128 | b.Run(test, func(b *testing.B) { |
| 129 | for b.Loop() { |
| 130 | _ = normalizeKey(test) |
| 131 | } |
| 132 | }) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // Golden tests |
| 137 | |
| 138 | func TestGolden_Load(t *testing.T) { |
| 139 | for _, tt := range []string{"different-cases", "full", "invalid-values", "missing", "non-table", "unknown-keys"} { |
| 140 | t.Run(tt, func(t *testing.T) { |
| 141 | ar := golden.Read(t, tt) |
| 142 | path := filepath.Join(t.TempDir(), "clerk.toml") |
| 143 | if cfg := ar.Get("config.toml"); cfg != nil { |
| 144 | if err := os.WriteFile(path, cfg, 0o644); err != nil { |
| 145 | t.Fatal(err) |
| 146 | } |
| 147 | } |
| 148 | s, warns, err := Load(path) |
| 149 | golden.Assert(t, ar, renderTOML(s, warns, err)) |
| 150 | }) |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | func renderTOML(s Settings, warns []string, err error) string { |
| 155 | var lines []string |
| 156 | for _, w := range warns { |
| 157 | lines = append(lines, "warning: "+w) |
| 158 | } |
| 159 | if err != nil { |
| 160 | for line := range strings.SplitSeq(err.Error(), "\n") { |
| 161 | if line != "" { |
| 162 | lines = append(lines, "error: "+line) |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | slices.Sort(lines) |
| 167 | if err == nil { |
| 168 | lines = append(lines, changedLines(configLines(s), configLines(DefaultConfig))...) |
| 169 | } |
| 170 | if len(lines) == 0 { |
| 171 | return "" |
| 172 | } |
| 173 | return strings.Join(lines, "\n") + "\n" |
| 174 | } |
| 175 | |
| 176 | func configLines(s Settings) []string { |
| 177 | var lines []string |
| 178 | for _, line := range renderFormat(s.Format) { |
| 179 | lines = append(lines, "format."+line) |
| 180 | } |
| 181 | for _, id := range sortedRuleIDs() { |
| 182 | rc := s.Linter.Rules[id] |
| 183 | line := "lint." + string(id) + ": " |
| 184 | if rc.Disabled { |
| 185 | line += "disabled" |
| 186 | } else { |
| 187 | line += rc.Severity.String() |
| 188 | if len(rc.Options) > 0 { |
| 189 | line += " options=" + string(rc.Options) |
| 190 | } |
| 191 | } |
| 192 | lines = append(lines, line) |
| 193 | } |
| 194 | return lines |
| 195 | } |
| 196 | |
| 197 | func changedLines(got, want []string) []string { |
| 198 | var out []string |
| 199 | for i, line := range got { |
| 200 | if line != want[i] { |
| 201 | out = append(out, line) |
| 202 | } |
| 203 | } |
| 204 | return out |
| 205 | } |
| 206 | |
| 207 | func renderFormat(c printer.Config) []string { |
| 208 | return []string{ |
| 209 | "tab-indent: " + fmt.Sprint(c.TabIndent), |
| 210 | "indent-width: " + fmt.Sprint(c.IndentWidth), |
| 211 | "preserve-blank-lines: " + fmt.Sprint(c.PreserveBlankLines), |
| 212 | "align-style: " + c.AlignStyle.String(), |
| 213 | "align-column: " + fmt.Sprint(c.AlignColumn), |
| 214 | "commodity-pos: " + c.CommodityPos.String(), |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | func sortedRuleIDs() []linter.RuleID { |
| 219 | ids := make([]linter.RuleID, 0, len(linter.Rules)) |
| 220 | for id := range linter.Rules { |
| 221 | ids = append(ids, id) |
| 222 | } |
| 223 | slices.Sort(ids) |
| 224 | return ids |
| 225 | } |