// 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) } } }