aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/mainconf_test.go
blob: c77591c7423a883cbc022461efe7e88b1d444d95 (plain) (blame)
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
// 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")
	}
}