summaryrefslogtreecommitdiff
path: root/cmd/krino/exclude_test.go
blob: f76be0c272e973488a7360a5127bca491e046af9 (plain) (blame)
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// 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)
	}
}

// TestVerboseListsDirectoriesLeftOutOfTheWalk: a rule's destination inside
// the directory is not walked, so its files never show in any count; -v
// says so, and only for directories that exist (triage 4).
func TestVerboseListsDirectoriesLeftOutOfTheWalk(t *testing.T) {
	h := home(t)
	dl := filepath.Join(h, "dl")
	old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	p := filepath.Join(dl, "Work", "Acme", "old-acme.pdf")
	os.MkdirAll(filepath.Dir(p), 0o755)
	os.WriteFile(p, []byte("x"), 0o644)
	os.Chtimes(p, old, old)
	if code, _, errOut := runCLI(t, "init"); code != 0 {
		t.Fatal(errOut)
	}
	if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
		t.Fatal(errOut)
	}
	rules := "(path \"~/dl\")\n(recursive yes)\n(rule \"acme\" (when (name \"acme\")) (move \"Work/Acme\"))\n(rule \"later\" (move \"Later/{mtime:%Y}\"))\n"
	os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644)
	_, out, errOut := runCLI(t, "-n", "-v")
	if !strings.Contains(out, "\nnot scanned (a rule's destination)\n  Work/Acme/\n") {
		t.Errorf("-v lacks the destination left out of the walk:\n%s\n%s", out, errOut)
	}
	if strings.Contains(out, "Later/") {
		t.Errorf("a destination that does not exist is listed:\n%s", out)
	}
	if _, out, _ = runCLI(t, "-n"); strings.Contains(out, "not scanned") {
		t.Errorf("listed without -v:\n%s", out)
	}
}

// TestExplainShowsAnUndecidedRule: explain names a rule it could not decide
// "undecided", not "no" (plan 12).
func TestExplainShowsAnUndecidedRule(t *testing.T) {
	h := home(t)
	dl := filepath.Join(h, "dl")
	os.MkdirAll(dl, 0o755)
	p := filepath.Join(dl, "big.txt")
	os.WriteFile(p, []byte("confidential "+strings.Repeat("x", 2048)), 0o644)
	old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	os.Chtimes(p, old, old)
	if code, _, errOut := runCLI(t, "init"); code != 0 {
		t.Fatal(errOut)
	}
	if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
		t.Fatal(errOut)
	}
	os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte("(path \"~/dl\")\n(max-read 1K)\n(rule \"keep\" (when (content \"confidential\")) (stop))\n(rule \"all\" (move \"Out\"))\n"), 0o644)
	_, out, errOut := runCLI(t, "explain", p)
	if !strings.Contains(out, "rule keep: undecided") || !strings.Contains(out, "rule all: not evaluated, stopped by rule keep, which could not be decided") {
		t.Errorf("explain output:\n%s\n%s", out, errOut)
	}
}