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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
#!/bin/bash
# test-zfs-snap-prune.sh - Comprehensive test suite for zfs-snap-prune
#
# Runs various scenarios with mock data to verify the pruning logic.
# No root or ZFS required - uses --test mode with mock data.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRUNE_SCRIPT="$SCRIPT_DIR/../custom/zfs-snap-prune"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Test counters - use temp files to avoid subshell issues with pipes
COUNTER_FILE=$(mktemp)
echo "0 0 0" > "$COUNTER_FILE" # run passed failed
trap "rm -f $COUNTER_FILE" EXIT
# Time constants for generating test data
DAY=$((24 * 60 * 60))
NOW=$(date +%s)
# Generate a snapshot line for mock data
# Args: snapshot_name days_ago
# Output: zroot/ROOT/default@name<TAB>epoch_timestamp
make_snap() {
local name="$1"
local days_ago="$2"
local timestamp=$((NOW - (days_ago * DAY)))
echo -e "zroot/ROOT/default@${name}\t${timestamp}"
}
# Generate N snapshots with given prefix and starting age
# Args: prefix count start_days_ago
# NOTE: Outputs oldest first (like ZFS with -s creation), so start_age is the OLDEST
make_snaps() {
local prefix="$1"
local count="$2"
local start_age="$3"
# Generate from oldest to newest (ZFS order with -s creation)
for ((i=count; i>=1; i--)); do
make_snap "${prefix}_${i}" $((start_age + i - 1))
done
}
# Increment counter in temp file
# Args: position (1=run, 2=passed, 3=failed)
inc_counter() {
local pos="$1"
local counters
read -r run passed failed < "$COUNTER_FILE"
case "$pos" in
1) run=$((run + 1)) ;;
2) passed=$((passed + 1)) ;;
3) failed=$((failed + 1)) ;;
esac
echo "$run $passed $failed" > "$COUNTER_FILE"
}
# Get counter values
get_counters() {
cat "$COUNTER_FILE"
}
# Run a test case
# Args: test_name expected_kept expected_deleted env_vars
# Reads mock snapshot data from stdin
run_test() {
local test_name="$1"
local expected_kept="$2"
local expected_deleted="$3"
shift 3
local env_vars="$*"
inc_counter 1 # TESTS_RUN++
echo -e "${BLUE}TEST:${NC} $test_name"
# Capture stdin (mock data) and pass to prune script
local mock_data
mock_data=$(cat)
# Run prune script with mock data on stdin
local output
output=$(echo "$mock_data" | env NOW_OVERRIDE="$NOW" $env_vars "$PRUNE_SCRIPT" --test --quiet 2>&1)
# Extract results
local result_line
result_line=$(echo "$output" | grep "^RESULT:" || echo "RESULT:kept=0,deleted=0")
local actual_kept
actual_kept=$(echo "$result_line" | sed 's/.*kept=\([0-9]*\).*/\1/')
local actual_deleted
actual_deleted=$(echo "$result_line" | sed 's/.*deleted=\([0-9]*\).*/\1/')
# Compare
if [[ "$actual_kept" == "$expected_kept" ]] && [[ "$actual_deleted" == "$expected_deleted" ]]; then
echo -e " ${GREEN}PASS${NC} (kept=$actual_kept, deleted=$actual_deleted)"
inc_counter 2 # TESTS_PASSED++
return 0
else
echo -e " ${RED}FAIL${NC}"
echo -e " Expected: kept=$expected_kept, deleted=$expected_deleted"
echo -e " Actual: kept=$actual_kept, deleted=$actual_deleted"
inc_counter 3 # TESTS_FAILED++
return 1
fi
}
# Print section header
section() {
echo ""
echo -e "${YELLOW}=== $1 ===${NC}"
}
# Verify prune script exists
if [[ ! -x "$PRUNE_SCRIPT" ]]; then
chmod +x "$PRUNE_SCRIPT"
fi
echo -e "${GREEN}zfs-snap-prune Test Suite${NC}"
echo "========================="
echo "Using NOW=$NOW ($(date -d "@$NOW" '+%Y-%m-%d %H:%M:%S'))"
echo "Default policy: KEEP_COUNT=20, MAX_AGE_DAYS=180"
###############################################################################
section "Basic Cases"
###############################################################################
# Test 1: Empty list
echo -n "" | run_test "Empty snapshot list" 0 0
# Test 2: Single snapshot
make_snap "test1" 5 | run_test "Single snapshot (recent)" 1 0
# Test 3: Under keep count - all recent
make_snaps "recent" 10 1 | run_test "10 snapshots, all recent" 10 0
# Test 4: Exactly at keep count
make_snaps "exact" 20 1 | run_test "Exactly 20 snapshots" 20 0
###############################################################################
section "Over Keep Count - Age Matters"
###############################################################################
# Test 5: 25 snapshots, all recent (within 180 days)
# Should keep all - none old enough to delete
make_snaps "recent" 25 1 | run_test "25 snapshots, all recent (<180 days)" 25 0
# Test 6: 25 snapshots, 5 are old (>180 days)
# First 20 (most recent) kept by count, 5 oldest are >180 days old, so deleted
{
make_snaps "old" 5 200 # oldest first (200-204 days ago)
make_snaps "recent" 20 1 # newest last (1-20 days ago)
} | run_test "25 snapshots, 5 old (>180 days) - delete 5" 20 5
# Test 7: 30 snapshots, 10 beyond limit but only 5 old enough
{
make_snaps "old" 5 200 # 200-204 days old - delete these
make_snaps "medium" 5 100 # 100-104 days old - not old enough
make_snaps "recent" 20 1 # 1-20 days old
} | run_test "30 snapshots, 5 medium age, 5 old - delete 5" 25 5
###############################################################################
section "Genesis Protection"
###############################################################################
# Test 8: Genesis at position 21, old - should NOT be deleted
{
make_snap "genesis" 365 # oldest: 1 year old
make_snaps "recent" 20 1 # newest: 1-20 days ago
} | run_test "Genesis at position 21 (old) - protected" 21 0
# Test 9: Genesis at position 25, with other old snapshots
{
make_snap "genesis" 365 # oldest: protected
make_snaps "old" 4 200 # 200-203 days old - should be deleted
make_snaps "recent" 20 1 # 1-20 days old
} | run_test "Genesis at position 25 with 4 old - delete 4, keep genesis" 21 4
# Test 10: Genesis within keep count (20 total snapshots)
{
make_snap "genesis" 365 # oldest
make_snaps "more" 9 15 # 15-23 days ago
make_snaps "recent" 10 1 # 1-10 days ago
} | run_test "Genesis at position 11 (within keep count)" 20 0
###############################################################################
section "Custom Configuration"
###############################################################################
# Test 11: Custom KEEP_COUNT=5
make_snaps "test" 10 200 | \
run_test "KEEP_COUNT=5, 10 old snapshots - delete 5" 5 5 KEEP_COUNT=5
# Test 12: Custom MAX_AGE_DAYS=30
{
make_snaps "medium" 5 50 # 50-54 days old - now considered old with MAX_AGE=30
make_snaps "recent" 20 1 # 1-20 days ago
} | run_test "MAX_AGE_DAYS=30, 5 snapshots >30 days - delete 5" 20 5 MAX_AGE_DAYS=30
# Test 13: Very short retention
make_snaps "test" 15 10 | \
run_test "KEEP_COUNT=3, MAX_AGE=7, 15 snaps (10+ days old) - delete 12" 3 12 KEEP_COUNT=3 MAX_AGE_DAYS=7
# Test 14: Relaxed retention - nothing deleted
make_snaps "test" 50 1 | \
run_test "KEEP_COUNT=100 - keep all 50" 50 0 KEEP_COUNT=100
###############################################################################
section "Edge Cases"
###############################################################################
# Test 15: Snapshot exactly at MAX_AGE boundary (180 days) - should be kept
{
make_snap "boundary" 180 # Exactly 180 days - >= cutoff, kept
make_snaps "recent" 20 1 # 1-20 days ago
} | run_test "1 snapshot exactly at 180 day boundary - keep" 21 0
# Test 16: Snapshot just over MAX_AGE boundary (181 days) - should be deleted
{
make_snap "over" 181 # 181 days - just over, should be deleted
make_snaps "recent" 20 1 # 1-20 days ago
} | run_test "1 snapshot at 181 days - delete" 20 1
# Test 17: Mixed boundary - some at 180, some at 181
{
make_snap "over2" 182 # deleted
make_snap "over1" 181 # deleted
make_snap "boundary" 180 # kept (exactly at cutoff)
make_snaps "recent" 20 1 # 1-20 days ago
} | run_test "2 over boundary, 1 at boundary - delete 2" 21 2
# Test 18: Mixed naming patterns (ordered oldest to newest)
{
make_snap "genesis" 365
make_snap "before-upgrade" 20
make_snap "manual_backup" 15
make_snap "pre-pacman_2025-01-10" 10
make_snap "pre-pacman_2025-01-15" 5
} | run_test "Mixed snapshot names (5 total)" 5 0
# Test 19: Large number of snapshots
{
make_snaps "old" 100 200 # 200-299 days old
make_snaps "recent" 20 1 # 1-20 days ago
} | run_test "120 snapshots, 100 old - delete 100" 20 100
###############################################################################
section "Realistic Scenarios"
###############################################################################
# Test 20: One year of weekly pacman updates + genesis
# 52 snapshots (one per week) + genesis
# Ordered oldest first: genesis (365 days), then week_51 (357 days), ..., week_0 (0 days)
{
make_snap "genesis" 365
for ((week=51; week>=0; week--)); do
make_snap "pre-pacman_week_${week}" $((week * 7))
done
} | run_test "1 year of weekly updates (52) + genesis" 27 26
# Analysis (after tac, newest first):
# Position 1-20: week_0 through week_19 (0-133 days) - kept by count
# Position 21-26: week_20 through week_25 (140-175 days) - kept by age (<180)
# Position 27-52: week_26 through week_51 (182-357 days) - deleted (>180)
# Position 53: genesis (365 days) - protected
# Kept: 20 + 6 + 1 = 27, Deleted: 26
# Test 21: Fresh install with only genesis
make_snap "genesis" 1 | run_test "Fresh install - only genesis" 1 0
# Test 22: Burst of manual snapshots before big change
{
make_snap "genesis" 30
make_snap "before-nvidia" 20
make_snap "before-DE-change" 19
make_snaps "pre-pacman" 18 1
} | run_test "20 snaps + genesis (30 days old)" 21 0
###############################################################################
section "Results"
###############################################################################
# Read final counters
read -r TESTS_RUN TESTS_PASSED TESTS_FAILED < "$COUNTER_FILE"
echo ""
echo "========================="
echo -e "Tests run: $TESTS_RUN"
echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
echo ""
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}All tests passed!${NC}"
exit 0
else
echo -e "${RED}Some tests failed.${NC}"
exit 1
fi
|