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
|
// SPDX-License-Identifier: GPL-3.0-or-later
package config
import (
"reflect"
"strings"
"testing"
)
const fullDir = `;; -*- mode: lisp -*-
(path "~/downloads")
(recursive yes)
(ignore "*.part" "*.aria2")
(ignore ".*")
(rule "acme"
(when (type document)
(or (content "acme ltd") (name "\bacme\b")))
(move "Work/Acme/{mtime:%Y}")
(stop))
(rule "photos"
(case strict)
(when (type image))
(rename "{mtime:%Y-%m-%d}_{stem}{ext}")
(move "Photos"))
(rule "old" (when (type package) (age > 30d)) (delete permanent))
(rule "keep" (when (name "^important")) (stop))
(rule "all" (copy "~/backup"))
`
func kinds(r *Rule) []ActionKind {
var k []ActionKind
for _, a := range r.Actions {
k = append(k, a.Kind)
}
return k
}
func TestParseDir(t *testing.T) {
t.Setenv("HOME", "/home/u")
dir, errs := ParseDir("downloads", "downloads.conf", []byte(fullDir))
if len(errs) > 0 {
t.Fatal(errs)
}
if dir.Name != "downloads" || dir.Path != "/home/u/downloads" || dir.PathText != "~/downloads" {
t.Errorf("dir = %+v", dir)
}
if !reflect.DeepEqual(dir.Ignore, []string{"*.part", "*.aria2", ".*"}) {
t.Errorf("Ignore = %q", dir.Ignore)
}
if !dir.Settings.Over(Builtin()).Recursive {
t.Error("recursive not set")
}
if len(dir.Rules) != 5 {
t.Fatalf("got %d rules", len(dir.Rules))
}
acme := dir.Rules[0]
if acme.Name != "acme" || !acme.Stop || !acme.HasWhen || len(acme.When) != 2 || acme.When[1].Head() != "or" {
t.Errorf("acme = %+v", acme)
}
if len(acme.Actions) != 1 || acme.Actions[0].Kind != Move || acme.Actions[0].Arg != "Work/Acme/{mtime:%Y}" {
t.Errorf("acme actions = %+v", acme.Actions)
}
photos := dir.Rules[1]
if photos.Settings.Over(Builtin()).Case != CaseStrict {
t.Error("photos: case strict not set")
}
if got := kinds(photos); !reflect.DeepEqual(got, []ActionKind{Rename, Move}) {
t.Errorf("photos actions = %v", got)
}
if got := kinds(dir.Rules[2]); !reflect.DeepEqual(got, []ActionKind{DeletePermanent}) {
t.Errorf("old actions = %v", got)
}
if keep := dir.Rules[3]; !keep.Stop || len(keep.Actions) != 0 {
t.Errorf("keep = %+v", keep)
}
if all := dir.Rules[4]; all.HasWhen || all.When != nil || all.Actions[0].Arg != "~/backup" {
t.Errorf("all = %+v", all)
}
}
func TestActionKindString(t *testing.T) {
want := map[ActionKind]string{Copy: "copy", Move: "move", Rename: "rename", Delete: "delete", DeletePermanent: "delete permanent"}
for k, s := range want {
if k.String() != s {
t.Errorf("%d.String() = %q, want %q", k, k.String(), s)
}
}
}
func TestParseDirErrors(t *testing.T) {
tests := []struct{ src, want string }{
{``, `d.conf: no (path ...): say which directory this file sorts`},
{`(path ~/x)`, `d.conf:1:7: path must be a string: write (path "~/x")`},
{`(path "rel")`, `d.conf:1:7: path must be absolute or start with ~, not "rel"`},
{`(path "/a") (path "/b")`, `d.conf:1:13: path given twice (first at line 1)`},
{`(path "/a") (ignore part)`, `d.conf:1:21: ignore takes patterns in quotes, like "*.part"; got part`},
{`(path "/a") (rule x (stop))`, `d.conf:1:13: rule needs a name in quotes first, like (rule "invoices" ...)`},
{`(path "/a") (rule "x" (stop)) (rule "x" (stop))`, `d.conf:1:31: rule "x" defined twice (first at line 1)`},
{`(path "/a") (rule "x" (when (type pdf)))`, `d.conf:1:13: rule "x" does nothing: give it an action like (move "Somewhere") or (stop)`},
{`(path "/a") (rule "x" (when) (stop))`, `d.conf:1:23: rule "x": (when) needs a condition; leave it out to match every file`},
{`(path "/a") (rule "x" (when pdf) (stop))`, `d.conf:1:29: rule "x": a condition is a form like (type pdf), not pdf`},
{`(path "/a") (rule "x" (when (a)) (when (b)) (stop))`, `d.conf:1:34: rule "x": when given twice (first at line 1)`},
{`(path "/a") (rule "x" (recursive yes) (stop))`, `d.conf:1:23: rule "x": recursive cannot be set in a rule, only case, fold and on-conflict`},
{`(path "/a") (rule "x" (move Work))`, `d.conf:1:29: rule "x": move takes a string: write (move "Work")`},
{`(path "/a") (rule "x" (move))`, `d.conf:1:23: rule "x": move takes one directory in quotes`},
{`(path "/a") (rule "x" (rename "a/b"))`, `d.conf:1:31: rule "x": rename gives a new name, not a path; use move to change directory`},
{`(path "/a") (rule "x" (delete forever))`, `d.conf:1:23: rule "x": write (delete) for the Trash or (delete permanent)`},
{`(path "/a") (rule "x" (delete) (move "y"))`, `d.conf:1:32: rule "x": (move "y") after delete would never run`},
{`(path "/a") (rule "x" (stop now))`, `d.conf:1:23: rule "x": stop takes nothing: write (stop)`},
{`(path "/a") (rule "x" (fly "y"))`, `d.conf:1:23: rule "x": unknown form (fly ...); a rule has when, copy, move, rename, delete, stop, case, fold and on-conflict`},
{`(path "/a") (rule "(review)" (delete))`, `d.conf:1:13: rule names cannot start with "(": (review) marks choices made in review`},
{`(path "/a") (sort "x")`, `d.conf:1:13: unknown form (sort ...); a directory file has path, ignore, exclude, rule and settings like (recursive yes)`},
}
for _, tt := range tests {
_, errs := ParseDir("d", "d.conf", []byte(tt.src))
if len(errs) != 1 || errs[0].Error() != tt.want {
t.Errorf("%s:\n got %v\n want %s", tt.src, errs, tt.want)
}
}
}
// TestParseExclude: (exclude COND...) may repeat; each form keeps its
// conditions (all must hold, as in when) and its text as written, with the
// whitespace collapsed, for check and explain to show.
func TestParseExclude(t *testing.T) {
src := `(path "/tmp")
(exclude (type iso img))
(exclude (name "^draft")
(content "poufne"))
`
dir, errs := ParseDir("dl", "dl.conf", []byte(src))
if len(errs) != 0 {
t.Fatal(errs)
}
if len(dir.Excludes) != 2 || len(dir.Excludes[0].When) != 1 || len(dir.Excludes[1].When) != 2 {
t.Fatalf("Excludes = %+v", dir.Excludes)
}
if got, want := dir.Excludes[1].Text, `(exclude (name "^draft") (content "poufne"))`; got != want {
t.Errorf("Text = %q, want %q", got, want)
}
if dir.Excludes[1].Pos.Line != 3 {
t.Errorf("Pos = %+v, want line 3", dir.Excludes[1].Pos)
}
for _, tt := range []struct{ src, want string }{
{"(path \"/tmp\")\n(exclude)\n", "(exclude) needs a condition"},
{"(path \"/tmp\")\n(exclude iso)\n", "exclude: a condition is a form like (type pdf), not iso"},
} {
_, errs := ParseDir("dl", "dl.conf", []byte(tt.src))
if len(errs) != 1 || !strings.Contains(errs[0].Msg, tt.want) {
t.Errorf("%q: errs %v, want one containing %q", tt.src, errs, tt.want)
}
}
}
|