diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/dir.go | 4 | ||||
| -rw-r--r-- | internal/config/dir_test.go | 1 | ||||
| -rw-r--r-- | internal/engine/match.go | 2 | ||||
| -rw-r--r-- | internal/extract/extract.go | 5 | ||||
| -rw-r--r-- | internal/extract/tools_test.go | 15 | ||||
| -rw-r--r-- | internal/plan/conflict.go | 5 | ||||
| -rw-r--r-- | internal/plan/enum_test.go | 27 | ||||
| -rw-r--r-- | internal/scan/scan.go | 12 | ||||
| -rw-r--r-- | internal/scan/scan_test.go | 21 |
9 files changed, 88 insertions, 4 deletions
diff --git a/internal/config/dir.go b/internal/config/dir.go index 461d912..54ee35e 100644 --- a/internal/config/dir.go +++ b/internal/config/dir.go @@ -184,6 +184,10 @@ func parseRule(n *sexp.Node, d *diags) *Rule { d.at(n, `rule needs a name in quotes first, like (rule "invoices" ...)`) return nil } + if strings.HasPrefix(args[0].Text, "(") { + d.at(n, `rule names cannot start with "(": (review) marks choices made in review`) + return nil + } r := &Rule{Name: args[0].Text, Pos: n.Pos} seen := map[string]*sexp.Node{} var whenNode *sexp.Node diff --git a/internal/config/dir_test.go b/internal/config/dir_test.go index 080905c..cae5f5f 100644 --- a/internal/config/dir_test.go +++ b/internal/config/dir_test.go @@ -112,6 +112,7 @@ func TestParseDirErrors(t *testing.T) { {`(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 { diff --git a/internal/engine/match.go b/internal/engine/match.go index fedc053..162730b 100644 --- a/internal/engine/match.go +++ b/internal/engine/match.go @@ -422,7 +422,7 @@ func explainSkip(d *Dir, sf scan.File, excl []string, now time.Time) string { if isBusy(sf.Path, d.Settings.Busy) { return "busy" } - if now.Sub(sf.ModTime) < d.Settings.MinAge { + if scan.Age(now, sf.ModTime) < d.Settings.MinAge { return "too new" } if d.Settings.MaxSize > 0 && sf.Size > d.Settings.MaxSize { diff --git a/internal/extract/extract.go b/internal/extract/extract.go index 9768db1..ad51c00 100644 --- a/internal/extract/extract.go +++ b/internal/extract/extract.go @@ -99,7 +99,10 @@ func newWithPath(path string) *Extractor { tools := make(map[string]string, len(toolNames)) for _, name := range toolNames { for _, dir := range dirs { - if dir == "" { + // A relative entry would find a tool relative to the working + // directory - a bin/pdftotext an unpacked download left behind + // (review planapply F7) - so only absolute entries count. + if dir == "" || !filepath.IsAbs(dir) { continue } p := filepath.Join(dir, name) diff --git a/internal/extract/tools_test.go b/internal/extract/tools_test.go index b481e8a..756903d 100644 --- a/internal/extract/tools_test.go +++ b/internal/extract/tools_test.go @@ -261,3 +261,18 @@ func TestFingerprintFollowsTools(t *testing.T) { t.Error("replacing pdftotext did not change the fingerprint") } } + +// TestToolLookupSkipsRelativePathEntries: a relative PATH entry would make +// a tool's path relative to the working directory - a bin/pdftotext left by +// an unpacked download - so it is ignored (review planapply F7). +func TestToolLookupSkipsRelativePathEntries(t *testing.T) { + wd := t.TempDir() + if err := os.Mkdir(filepath.Join(wd, "bin"), 0o755); err != nil { + t.Fatal(err) + } + fakeTool(t, filepath.Join(wd, "bin"), "pdftotext", "echo injected") + t.Chdir(wd) + if e := newWithPath("bin"); e.tools["pdftotext"] != "" { + t.Errorf("tool found through a relative PATH entry: %q", e.tools["pdftotext"]) + } +} diff --git a/internal/plan/conflict.go b/internal/plan/conflict.go index c4c6b64..c660a8b 100644 --- a/internal/plan/conflict.go +++ b/internal/plan/conflict.go @@ -120,10 +120,13 @@ func resolveConflict(kind Kind, policy config.Conflict, src, dst string, d Disk, // displaces nothing. resolved, skip := suffixed(dst, d, c) return resolved, skip, "" - default: // config.ConflictSuffix + case config.ConflictSuffix: resolved, skip := suffixed(dst, d, c) return resolved, skip, "" } + // Every policy has its own branch (review cli F13): a new one must not + // quietly plan as another. + panic(fmt.Sprintf("plan: unknown config.Conflict %d", int(policy))) } // maxSuffixAttempts bounds suffixed(): it is unbounded by design and diff --git a/internal/plan/enum_test.go b/internal/plan/enum_test.go index 6458f5f..5e4f645 100644 --- a/internal/plan/enum_test.go +++ b/internal/plan/enum_test.go @@ -45,3 +45,30 @@ func TestEveryKindIsNamed(t *testing.T) { }() } } + +// TestEveryConflictPolicyIsPlanned: every config.Conflict value is resolved +// by its own branch; an unknown value panics instead of quietly planning as +// suffix (review cli F13). Conflict is an iota block from 0. +func TestEveryConflictPolicyIsPlanned(t *testing.T) { + policies, err := enumtest.Names("../config/settings.go", "Conflict") + if err != nil { + t.Fatal(err) + } + d := fakeDisk{exists: map[string]bool{"/r/b.pdf": true}} + for i, name := range policies { + func() { + defer func() { + if r := recover(); r != nil { + t.Errorf("config.%s is not planned: %v", name, r) + } + }() + resolveConflict(Move, config.Conflict(i), "/r/a.pdf", "/r/b.pdf", d, claimed{}) + }() + } + defer func() { + if recover() == nil { + t.Error("an unknown conflict policy did not panic") + } + }() + resolveConflict(Move, config.Conflict(len(policies)), "/r/a.pdf", "/r/b.pdf", d, claimed{}) +} diff --git a/internal/scan/scan.go b/internal/scan/scan.go index 77cca66..bd717e0 100644 --- a/internal/scan/scan.go +++ b/internal/scan/scan.go @@ -229,7 +229,7 @@ func (w *walker) walk(dir, relDir string, depth int, entries []os.DirEntry) erro w.result.Skipped = append(w.result.Skipped, Skipped{Rel: rel, Reason: Busy}) continue } - if w.opt.Now.Sub(info.ModTime()) < w.opt.MinAge { + if Age(w.opt.Now, info.ModTime()) < w.opt.MinAge { w.result.Skipped = append(w.result.Skipped, Skipped{Rel: rel, Reason: TooNew}) continue } @@ -252,3 +252,13 @@ func (w *walker) isBusy(name string, names map[string]bool) bool { } return false } + +// Age is how long ago mtime was, at now. A modification time ahead of the +// clock (a skewed server, an archive's timestamps) counts as brand new: age +// 0, so min-age 0 still considers the file (review cli F5). +func Age(now, mtime time.Time) time.Duration { + if a := now.Sub(mtime); a > 0 { + return a + } + return 0 +} diff --git a/internal/scan/scan_test.go b/internal/scan/scan_test.go index e9aeb73..35e7a0d 100644 --- a/internal/scan/scan_test.go +++ b/internal/scan/scan_test.go @@ -319,3 +319,24 @@ func TestFilesCarryInode(t *testing.T) { t.Errorf("rename changed the identity: %+v then %+v", first.Files[0], second.Files) } } + +// TestFutureFileIsNotTooNewAtMinAgeZero: a file whose modification time is +// ahead of the clock counts as brand new - skipped while min-age is above +// zero, considered at min-age 0 (review cli F5). +func TestFutureFileIsNotTooNewAtMinAgeZero(t *testing.T) { + root := tree(t) + p := filepath.Join(root, "future.txt") + if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + ahead := now.Add(time.Hour) + if err := os.Chtimes(p, ahead, ahead); err != nil { + t.Fatal(err) + } + if r, _ := Walk(root, Options{Now: now}); len(rels(r)) != 1 { + t.Errorf("min-age 0: files %v, skipped %v; want future.txt considered", rels(r), skipped(r)) + } + if r, _ := Walk(root, Options{Now: now, MinAge: time.Minute}); skipped(r)["future.txt"] != TooNew { + t.Errorf("min-age 1m: skipped %v; want future.txt too new", skipped(r)) + } +} |
