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
|
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"krino/internal/engine"
"krino/internal/scan"
)
// excludeFixture makes ~/dl with an old a.iso, an old keep.txt and a fresh
// new.txt, a krino.conf excluding iso files everywhere, and dl's own rules:
// exclude names starting with "draft", move everything else to Out.
func excludeFixture(t *testing.T) string {
t.Helper()
h := home(t)
dl := filepath.Join(h, "dl")
old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for name, mt := range map[string]time.Time{"a.iso": old, "keep.txt": old, "draft.txt": old, "new.txt": time.Now()} {
p := filepath.Join(dl, name)
if err := os.MkdirAll(dl, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(p, mt, mt); err != nil {
t.Fatal(err)
}
}
if code, _, errOut := runCLI(t, "init"); code != 0 {
t.Fatal(errOut)
}
if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
t.Fatal(errOut)
}
cfg := filepath.Join(h, ".config", "krino")
mainConf, err := os.ReadFile(filepath.Join(cfg, "krino.conf"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cfg, "krino.conf"), append(mainConf, []byte("\n(exclude (type iso))\n")...), 0o644); err != nil {
t.Fatal(err)
}
rules := "(path \"~/dl\")\n(exclude (name \"^draft\"))\n(rule \"all\" (move \"Out\"))\n"
if err := os.WriteFile(filepath.Join(cfg, "dirs", "dl.conf"), []byte(rules), 0o644); err != nil {
t.Fatal(err)
}
return h
}
// TestMinAgeFlagOverridesTheSetting: --min-age changes "too new" for one
// run, in either position, and a bad value is a usage error before anything
// is read.
func TestMinAgeFlagOverridesTheSetting(t *testing.T) {
excludeFixture(t)
_, out, _ := runCLI(t, "-n")
if strings.Contains(out, "new.txt") || !strings.Contains(out, "1 too new") {
t.Errorf("without the flag new.txt should be too new:\n%s", out)
}
for _, args := range [][]string{{"--min-age", "0", "-n"}, {"-n", "--min-age", "0s"}} {
code, out, errOut := runCLI(t, args...)
if code != 0 || !strings.Contains(out, " new.txt\n") || strings.Contains(out, "too new") {
t.Errorf("%v: exit %d, new.txt should be planned:\n%s\n%s", args, code, out, errOut)
}
}
code, _, errOut := runCLI(t, "--min-age", "soon", "-n")
if code != 2 || !strings.Contains(errOut, "--min-age") {
t.Errorf("--min-age soon: exit %d, stderr %q; want 2 naming --min-age", code, errOut)
}
}
// TestVerboseListsExcludedFiles: with -v, the plan lists each excluded file
// with the exclude that set it aside; the counts line counts them.
func TestVerboseListsExcludedFiles(t *testing.T) {
excludeFixture(t)
code, out, errOut := runCLI(t, "-n", "-v")
if code != 0 {
t.Fatalf("exit %d: %s", code, errOut)
}
for _, want := range []string{
"2 excluded",
"\nexcluded\n a.iso (exclude (type iso))\n draft.txt (exclude (name \"^draft\"))\n",
} {
if !strings.Contains(out, want) {
t.Errorf("output lacks %q:\n%s", want, out)
}
}
}
// TestCheckListsExclusions: check shows each directory's exclusions, the
// krino.conf ones first, before its rules.
func TestCheckListsExclusions(t *testing.T) {
excludeFixture(t)
code, out, errOut := runCLI(t, "check")
if code != 0 {
t.Fatalf("exit %d: %s", code, errOut)
}
want := " (exclude (type iso))\n (exclude (name \"^draft\"))\n 1 all"
if !strings.Contains(out, want) {
t.Errorf("check output lacks %q:\n%s", want, out)
}
}
// TestExplainShowsExclusion: explain says which exclude sets the file
// aside and traces every exclude.
func TestExplainShowsExclusion(t *testing.T) {
h := excludeFixture(t)
code, out, errOut := runCLI(t, "explain", filepath.Join(h, "dl", "a.iso"))
if code != 0 {
t.Fatalf("exit %d: %s", code, errOut)
}
for _, want := range []string{
"krino would set this file aside: (exclude (type iso))\n",
"(exclude (type iso)): MATCH\n",
"(exclude (name \"^draft\")): no\n",
} {
if !strings.Contains(out, want) {
t.Errorf("explain output lacks %q:\n%s", want, out)
}
}
}
// TestSkipSummaryCountsTooBig: a file skipped for max-size is counted as
// too big in the "not acted on" line.
func TestSkipSummaryCountsTooBig(t *testing.T) {
r := &engine.Result{Skipped: []scan.Skipped{{Rel: "big.iso", Reason: scan.TooBig}}}
if got := skipSummaryLine(r, nil, false); !strings.Contains(got, "1 too big") {
t.Errorf("line = %q, want 1 too big", got)
}
}
|