1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
// SPDX-License-Identifier: GPL-3.0-or-later
package model
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"git.labunix.xyz/krino/internal/engine"
)
// mainConf opens krino.conf over a sandbox whose main file holds text.
func mainConf(t *testing.T, main string) (*MainConf, *engine.Engine, string) {
t.Helper()
e, h := sandboxDir(t, "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n",
map[string]string{"a.pdf": "one"})
if main != "" {
if err := os.WriteFile(e.MainFile, []byte(main), 0o644); err != nil {
t.Fatal(err)
}
reloaded, diags := engine.Load(e.MainFile)
if len(diags) > 0 {
t.Fatalf("the fixture does not load: %v", diags)
}
e = reloaded
}
m, err := OpenMain(e)
if err != nil {
t.Fatal(err)
}
return m, e, h
}
// TestMainSettingsReadAndWrite: a default is read as written, changed in
// place, and taken out again, with everything else in the file untouched.
func TestMainSettingsReadAndWrite(t *testing.T) {
main := ";; the main file\n(include \"dl\")\n\n(defaults\n (min-age 2m) ; leave fresh files\n (on-conflict suffix))\n"
m, _, _ := mainConf(t, main)
if args, set, err := m.Setting("min-age"); err != nil || !set || args != "2m" {
t.Errorf("min-age = %q, %v, %v", args, set, err)
}
if _, set, _ := m.Setting("fold"); set {
t.Error("fold reads as set")
}
if err := m.SetSetting("min-age", "1d"); err != nil {
t.Fatal(err)
}
if !strings.Contains(m.Text, "(min-age 1d)") || strings.Contains(m.Text, "(min-age 2m)") {
t.Errorf("min-age not rewritten:\n%s", m.Text)
}
for _, keep := range []string{";; the main file", "(include \"dl\")", "; leave fresh files",
"(on-conflict suffix)"} {
if !strings.Contains(m.Text, keep) {
t.Errorf("writing one setting lost %q:\n%s", keep, m.Text)
}
}
// A setting the file does not have goes inside (defaults ...).
if err := m.SetSetting("fold", "no"); err != nil {
t.Fatal(err)
}
if !strings.Contains(m.Text, "(fold no)") {
t.Errorf("fold not written:\n%s", m.Text)
}
if strings.Index(m.Text, "(fold no)") < strings.Index(m.Text, "(defaults") {
t.Errorf("fold landed outside the defaults:\n%s", m.Text)
}
if diags := m.Check(); len(diags) > 0 {
t.Fatalf("the file no longer loads: %v", diags)
}
// And out again.
if err := m.SetSetting("min-age", ""); err != nil {
t.Fatal(err)
}
if strings.Contains(m.Text, "min-age") {
t.Errorf("min-age still there:\n%s", m.Text)
}
if diags := m.Check(); len(diags) > 0 {
t.Errorf("the file no longer loads: %v", diags)
}
}
// TestMainSettingsWithoutADefaultsForm: a file with no (defaults ...) gets
// one, and it loads.
func TestMainSettingsWithoutADefaultsForm(t *testing.T) {
m, _, _ := mainConf(t, "(include \"dl\")\n")
if err := m.SetSetting("recursive", "yes"); err != nil {
t.Fatal(err)
}
if !strings.Contains(m.Text, "(defaults") || !strings.Contains(m.Text, "(recursive yes)") {
t.Errorf("no defaults form was written:\n%s", m.Text)
}
if diags := m.Check(); len(diags) > 0 {
t.Errorf("the file no longer loads: %v", diags)
}
}
// TestMainLogIsTopLevel: the log path is not a default; it is written at
// the top level, where krino.conf(5) puts it.
func TestMainLogIsTopLevel(t *testing.T) {
m, _, h := mainConf(t, "(include \"dl\")\n")
if err := m.SetSetting("log", "\"~/tmp/krino.log\""); err != nil {
t.Fatal(err)
}
if strings.Contains(m.Text, "(defaults") {
t.Errorf("the log went into the defaults:\n%s", m.Text)
}
if diags := m.Check(); len(diags) > 0 {
t.Fatalf("the file no longer loads: %v", diags)
}
if err := m.Save(); err != nil {
t.Fatal(err)
}
e, diags := engine.Load(filepath.Join(h, ".config", "krino", "krino.conf"))
if len(diags) > 0 {
t.Fatal(diags)
}
if !strings.HasSuffix(e.Config.LogFile(), "tmp/krino.log") {
t.Errorf("the saved log path is not in effect: %s", e.Config.LogFile())
}
}
// TestMainSaveGuards: krino.conf is saved by the same rules as a directory
// file - never broken, never over someone else's edit, and the previous
// text is kept.
func TestMainSaveGuards(t *testing.T) {
main := "(include \"dl\")\n"
m, _, _ := mainConf(t, main)
m.Text = "(include \"dl\")\n(defaults (min-age nonsense))\n"
if err := m.Save(); err == nil {
t.Error("a file that will not load was saved")
}
if on, _ := os.ReadFile(m.File); string(on) != main {
t.Errorf("the refused save wrote anyway: %q", on)
}
m.Text = "(include \"dl\")\n(defaults (min-age 1d))\n"
if err := m.Save(); err != nil {
t.Fatal(err)
}
if on, _ := os.ReadFile(m.File + ".bak"); string(on) != main {
t.Errorf("backup = %q, want the previous text", on)
}
// Someone else edits it, and the next save refuses.
if err := os.WriteFile(m.File, []byte("(include \"dl\")\n;; theirs\n"), 0o644); err != nil {
t.Fatal(err)
}
m.Text = "(include \"dl\")\n;; mine\n"
if err := m.Save(); !errors.Is(err, ErrChangedOnDisk) {
t.Errorf("Save = %v, want ErrChangedOnDisk", err)
}
}
// TestMainSettingRefusesAnUnknownHead: only the settings krino.conf(5)
// documents can be written this way.
func TestMainSettingRefusesAnUnknownHead(t *testing.T) {
m, _, _ := mainConf(t, "(include \"dl\")\n")
if err := m.SetSetting("nonsense", "1"); err == nil {
t.Error("an unknown setting was accepted")
}
if _, _, err := m.Setting("nonsense"); err == nil {
t.Error("an unknown setting was read")
}
}
// TestClearingASettingKeepsTheRestOfItsLine: clearing removed whole lines,
// so a setting sharing a line with anything else took it too. The Settings
// window writes "(defaults\n (case ignore))" itself - the closing paren of
// defaults sits on the setting's line - so setting a value and putting it
// back to "default" broke the file it had just written, and every later
// change silently reverted.
func TestClearingASettingKeepsTheRestOfItsLine(t *testing.T) {
m, _, _ := mainConf(t, "(include \"dl\")\n")
if err := m.SetSetting("case", "ignore"); err != nil {
t.Fatal(err)
}
if err := m.SetSetting("case", ""); err != nil {
t.Fatal(err)
}
if diags := m.Check(); len(diags) > 0 {
t.Errorf("the text no longer loads after set-then-clear: %v\n%s", diags[0], m.Text)
}
// The other half: a hand-written one-line (defaults ...).
m2, _, _ := mainConf(t, "(include \"dl\")\n(defaults (case ignore) (fold yes))\n")
if err := m2.SetSetting("case", ""); err != nil {
t.Fatal(err)
}
if !strings.Contains(m2.Text, "(fold yes)") {
t.Errorf("clearing (case ...) took (fold yes) from the same line:\n%s", m2.Text)
}
if diags := m2.Check(); len(diags) > 0 {
t.Errorf("the text no longer loads: %v\n%s", diags[0], m2.Text)
}
}
|