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
|
// SPDX-License-Identifier: GPL-3.0-or-later
package config
import (
"reflect"
"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") (sort "x")`, `d.conf:1:13: unknown form (sort ...); a directory file has path, ignore, 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)
}
}
}
|