Skip to content

Commit 8bf3e17

Browse files
committed
Fix integration test isolation for CI reliability
Use zfs inherit instead of zfs set prop=- for property cleanup (the latter sets a literal "-" value). Add pre-test cleanup to all integration tests so leftover state from prior runs can't cause false failures. Use unique timestamps per test to prevent snapshot name collisions.
1 parent 5c1478e commit 8bf3e17

5 files changed

Lines changed: 94 additions & 63 deletions

File tree

internal/daemon/daemon_integration_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func clearAllProps(t *testing.T, client *zfs.Client) {
4141
"org.zvolta:snapshot-weekly", "org.zvolta:snapshot-monthly",
4242
"org.zvolta:snapshot-yearly",
4343
} {
44-
_ = client.SetProperty(ds, prop, "-")
44+
_ = client.InheritProperty(ds, prop)
4545
}
4646
}
4747
}
@@ -56,10 +56,17 @@ func destroyAllTestSnapshots(t *testing.T, mgr *snapshot.Manager, dataset string
5656
}
5757
}
5858

59+
func cleanAll(t *testing.T, d *Daemon) {
60+
t.Helper()
61+
clearAllProps(t, d.ZFS)
62+
destroyAllTestSnapshots(t, d.Snapshots, testDataset)
63+
destroyAllTestSnapshots(t, d.Snapshots, testPool)
64+
}
65+
5966
func TestIntegrationTickCreatesSnapshots(t *testing.T) {
6067
d := setupDaemon(t)
61-
defer clearAllProps(t, d.ZFS)
62-
defer destroyAllTestSnapshots(t, d.Snapshots, testDataset)
68+
cleanAll(t, d)
69+
defer cleanAll(t, d)
6370

6471
// Enable autosnap with hourly and daily on the dataset
6572
setProperty(t, d.ZFS, testDataset, "org.zvolta:autosnap", "on")
@@ -88,8 +95,8 @@ func TestIntegrationTickCreatesSnapshots(t *testing.T) {
8895

8996
func TestIntegrationTickIdempotentWithinBoundary(t *testing.T) {
9097
d := setupDaemon(t)
91-
defer clearAllProps(t, d.ZFS)
92-
defer destroyAllTestSnapshots(t, d.Snapshots, testDataset)
98+
cleanAll(t, d)
99+
defer cleanAll(t, d)
93100

94101
setProperty(t, d.ZFS, testDataset, "org.zvolta:autosnap", "on")
95102
setProperty(t, d.ZFS, testDataset, "org.zvolta:snapshot-hourly", "24")
@@ -119,8 +126,8 @@ func TestIntegrationTickIdempotentWithinBoundary(t *testing.T) {
119126

120127
func TestIntegrationTickCrossesHourBoundary(t *testing.T) {
121128
d := setupDaemon(t)
122-
defer clearAllProps(t, d.ZFS)
123-
defer destroyAllTestSnapshots(t, d.Snapshots, testDataset)
129+
cleanAll(t, d)
130+
defer cleanAll(t, d)
124131

125132
setProperty(t, d.ZFS, testDataset, "org.zvolta:autosnap", "on")
126133
setProperty(t, d.ZFS, testDataset, "org.zvolta:snapshot-hourly", "24")
@@ -148,8 +155,8 @@ func TestIntegrationTickCrossesHourBoundary(t *testing.T) {
148155

149156
func TestIntegrationTickPrunes(t *testing.T) {
150157
d := setupDaemon(t)
151-
defer clearAllProps(t, d.ZFS)
152-
defer destroyAllTestSnapshots(t, d.Snapshots, testDataset)
158+
cleanAll(t, d)
159+
defer cleanAll(t, d)
153160

154161
setProperty(t, d.ZFS, testDataset, "org.zvolta:autosnap", "on")
155162
setProperty(t, d.ZFS, testDataset, "org.zvolta:autoprune", "on")
@@ -185,8 +192,8 @@ func TestIntegrationTickPrunes(t *testing.T) {
185192

186193
func TestIntegrationTickFrequent(t *testing.T) {
187194
d := setupDaemon(t)
188-
defer clearAllProps(t, d.ZFS)
189-
defer destroyAllTestSnapshots(t, d.Snapshots, testDataset)
195+
cleanAll(t, d)
196+
defer cleanAll(t, d)
190197

191198
setProperty(t, d.ZFS, testDataset, "org.zvolta:autosnap", "on")
192199
setProperty(t, d.ZFS, testDataset, "org.zvolta:snapshot-frequent", "12")
@@ -228,7 +235,8 @@ func TestIntegrationTickFrequent(t *testing.T) {
228235

229236
func TestIntegrationTickDisabledDataset(t *testing.T) {
230237
d := setupDaemon(t)
231-
defer clearAllProps(t, d.ZFS)
238+
cleanAll(t, d)
239+
defer cleanAll(t, d)
232240

233241
// Don't set autosnap — should be disabled by default
234242
now := time.Date(2026, 6, 15, 14, 30, 0, 0, time.UTC)

internal/policy/policy_integration_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ func setProps(t *testing.T, client *zfs.Client, dataset string, props map[string
2828
func clearProps(t *testing.T, client *zfs.Client, dataset string) {
2929
t.Helper()
3030
for _, prop := range AllProperties() {
31-
_ = client.SetProperty(dataset, prop, "-")
31+
_ = client.InheritProperty(dataset, prop)
3232
}
3333
}
3434

3535
func TestIntegrationResolve(t *testing.T) {
3636
engine := testEngine()
3737
client := engine.ZFS
38+
clearProps(t, client, testPool)
39+
clearProps(t, client, testDataset)
3840

3941
setProps(t, client, testDataset, map[string]string{
4042
"org.zvolta:autosnap": "on",
@@ -77,6 +79,8 @@ func TestIntegrationResolve(t *testing.T) {
7779
func TestIntegrationInheritance(t *testing.T) {
7880
engine := testEngine()
7981
client := engine.ZFS
82+
clearProps(t, client, testPool)
83+
clearProps(t, client, testDataset)
8084

8185
// Set on parent pool
8286
setProps(t, client, testPool, map[string]string{
@@ -115,6 +119,8 @@ func TestIntegrationInheritance(t *testing.T) {
115119
func TestIntegrationResolveAll(t *testing.T) {
116120
engine := testEngine()
117121
client := engine.ZFS
122+
clearProps(t, client, testPool)
123+
clearProps(t, client, testDataset)
118124

119125
setProps(t, client, testPool, map[string]string{
120126
"org.zvolta:autosnap": "on",
@@ -128,7 +134,6 @@ func TestIntegrationResolveAll(t *testing.T) {
128134
t.Fatalf("ResolveAll: %v", err)
129135
}
130136

131-
// Should find at least the pool and child dataset
132137
if len(policies) < 2 {
133138
t.Errorf("expected at least 2 policies, got %d", len(policies))
134139
}
@@ -150,10 +155,8 @@ func TestIntegrationResolveAll(t *testing.T) {
150155
func TestIntegrationDefaultDisabled(t *testing.T) {
151156
engine := testEngine()
152157
client := engine.ZFS
153-
154-
// Clear everything — should default to autosnap=off
155-
clearProps(t, client, testDataset)
156158
clearProps(t, client, testPool)
159+
clearProps(t, client, testDataset)
157160

158161
p, err := engine.Resolve(testDataset)
159162
if err != nil {

internal/snapshot/manager_integration_test.go

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,31 @@ func testManager() *Manager {
2121
}
2222
}
2323

24+
// cleanSnapshots destroys all zvolta-managed snapshots on the test dataset.
25+
func cleanSnapshots(t *testing.T, mgr *Manager) {
26+
t.Helper()
27+
grouped, _ := mgr.ListManaged(testDataset)
28+
for _, snaps := range grouped {
29+
for _, s := range snaps {
30+
_ = mgr.ZFS.DestroySnapshot(s.Snapshot.Dataset, s.Snapshot.SnapName)
31+
}
32+
}
33+
}
34+
2435
func TestIntegrationCreateAndList(t *testing.T) {
2536
mgr := testManager()
26-
now := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
37+
cleanSnapshots(t, mgr)
38+
defer cleanSnapshots(t, mgr)
39+
40+
now := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)
2741

28-
// Create snapshots across multiple tiers
2942
tiers := []Tier{TierHourly, TierDaily}
3043
for _, tier := range tiers {
3144
if err := mgr.Create(testDataset, tier, now); err != nil {
3245
t.Fatalf("Create %s: %v", tier, err)
3346
}
3447
}
3548

36-
// List and verify
3749
grouped, err := mgr.ListManaged(testDataset)
3850
if err != nil {
3951
t.Fatalf("ListManaged: %v", err)
@@ -55,27 +67,22 @@ func TestIntegrationCreateAndList(t *testing.T) {
5567
t.Errorf("expected snapshot for tier %s at %v", tier, now)
5668
}
5769
}
58-
59-
// Clean up
60-
for _, tier := range tiers {
61-
name := FormatName("zvolta_", tier, now)
62-
_ = mgr.ZFS.DestroySnapshot(testDataset, name)
63-
}
6470
}
6571

6672
func TestIntegrationPrune(t *testing.T) {
6773
mgr := testManager()
74+
cleanSnapshots(t, mgr)
75+
defer cleanSnapshots(t, mgr)
6876

69-
// Create 5 hourly snapshots
70-
base := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
77+
// Use unique timestamps that don't overlap with other tests
78+
base := time.Date(2025, 2, 1, 10, 0, 0, 0, time.UTC)
7179
for i := 0; i < 5; i++ {
7280
ts := base.Add(time.Duration(i) * time.Hour)
7381
if err := mgr.Create(testDataset, TierHourly, ts); err != nil {
7482
t.Fatalf("Create: %v", err)
7583
}
7684
}
7785

78-
// Verify 5 exist
7986
grouped, err := mgr.ListManaged(testDataset)
8087
if err != nil {
8188
t.Fatalf("ListManaged: %v", err)
@@ -84,7 +91,6 @@ func TestIntegrationPrune(t *testing.T) {
8491
t.Fatalf("expected 5 hourly snapshots, got %d", len(grouped[TierHourly]))
8592
}
8693

87-
// Prune to keep 2
8894
removed, err := mgr.Prune(testDataset, TierHourly, 2, false)
8995
if err != nil {
9096
t.Fatalf("Prune: %v", err)
@@ -93,7 +99,6 @@ func TestIntegrationPrune(t *testing.T) {
9399
t.Errorf("expected 3 removed, got %d", len(removed))
94100
}
95101

96-
// Verify 2 remain (the newest)
97102
grouped, err = mgr.ListManaged(testDataset)
98103
if err != nil {
99104
t.Fatalf("ListManaged after prune: %v", err)
@@ -102,32 +107,27 @@ func TestIntegrationPrune(t *testing.T) {
102107
t.Errorf("expected 2 hourly after prune, got %d", len(grouped[TierHourly]))
103108
}
104109

105-
// Verify the remaining are the newest two (13:00 and 14:00)
110+
// Verify the remaining are the newest two (13:00 and 14:00 UTC)
106111
for _, s := range grouped[TierHourly] {
107112
if s.Parsed.Timestamp.Hour() < 13 {
108-
t.Errorf("expected only 13:00 and 14:00 to remain, got %v", s.Parsed.Timestamp)
113+
t.Errorf("expected only newest to remain, got %v", s.Parsed.Timestamp)
109114
}
110115
}
111-
112-
// Clean up remaining
113-
for _, s := range grouped[TierHourly] {
114-
_ = mgr.ZFS.DestroySnapshot(testDataset, s.Snapshot.SnapName)
115-
}
116116
}
117117

118118
func TestIntegrationDryRun(t *testing.T) {
119119
mgr := testManager()
120+
cleanSnapshots(t, mgr)
121+
defer cleanSnapshots(t, mgr)
120122

121-
// Create 3 snapshots
122-
base := time.Date(2026, 3, 5, 20, 0, 0, 0, time.UTC)
123+
base := time.Date(2025, 3, 1, 20, 0, 0, 0, time.UTC)
123124
for i := 0; i < 3; i++ {
124125
ts := base.Add(time.Duration(i) * time.Hour)
125126
if err := mgr.Create(testDataset, TierDaily, ts); err != nil {
126127
t.Fatalf("Create: %v", err)
127128
}
128129
}
129130

130-
// Dry-run prune to keep 1
131131
removed, err := mgr.Prune(testDataset, TierDaily, 1, true)
132132
if err != nil {
133133
t.Fatalf("Prune dry-run: %v", err)
@@ -136,30 +136,32 @@ func TestIntegrationDryRun(t *testing.T) {
136136
t.Errorf("dry-run should report 2 removals, got %d", len(removed))
137137
}
138138

139-
// Verify all 3 still exist (dry-run shouldn't delete)
140139
grouped, err := mgr.ListManaged(testDataset)
141140
if err != nil {
142141
t.Fatalf("ListManaged: %v", err)
143142
}
144143
if len(grouped[TierDaily]) != 3 {
145144
t.Errorf("dry-run should not delete, expected 3, got %d", len(grouped[TierDaily]))
146145
}
147-
148-
// Clean up
149-
for _, s := range grouped[TierDaily] {
150-
_ = mgr.ZFS.DestroySnapshot(testDataset, s.Snapshot.SnapName)
151-
}
152146
}
153147

154148
func TestIntegrationLastSnapshotTimes(t *testing.T) {
155149
mgr := testManager()
150+
cleanSnapshots(t, mgr)
151+
defer cleanSnapshots(t, mgr)
156152

157-
ts1 := time.Date(2026, 3, 5, 8, 0, 0, 0, time.UTC)
158-
ts2 := time.Date(2026, 3, 5, 9, 0, 0, 0, time.UTC)
153+
ts1 := time.Date(2025, 4, 1, 8, 0, 0, 0, time.UTC)
154+
ts2 := time.Date(2025, 4, 1, 9, 0, 0, 0, time.UTC)
159155

160-
_ = mgr.Create(testDataset, TierHourly, ts1)
161-
_ = mgr.Create(testDataset, TierHourly, ts2)
162-
_ = mgr.Create(testDataset, TierDaily, ts1)
156+
if err := mgr.Create(testDataset, TierHourly, ts1); err != nil {
157+
t.Fatalf("Create hourly ts1: %v", err)
158+
}
159+
if err := mgr.Create(testDataset, TierHourly, ts2); err != nil {
160+
t.Fatalf("Create hourly ts2: %v", err)
161+
}
162+
if err := mgr.Create(testDataset, TierDaily, ts1); err != nil {
163+
t.Fatalf("Create daily ts1: %v", err)
164+
}
163165

164166
times, err := mgr.LastSnapshotTimes(testDataset)
165167
if err != nil {
@@ -172,9 +174,4 @@ func TestIntegrationLastSnapshotTimes(t *testing.T) {
172174
if !times[TierDaily].Equal(ts1) {
173175
t.Errorf("daily last = %v, want %v", times[TierDaily], ts1)
174176
}
175-
176-
// Clean up
177-
_ = mgr.ZFS.DestroySnapshot(testDataset, FormatName("zvolta_", TierHourly, ts1))
178-
_ = mgr.ZFS.DestroySnapshot(testDataset, FormatName("zvolta_", TierHourly, ts2))
179-
_ = mgr.ZFS.DestroySnapshot(testDataset, FormatName("zvolta_", TierDaily, ts1))
180177
}

internal/zfs/zfs.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ func (c *Client) SetProperty(dataset, property, value string) error {
126126
return err
127127
}
128128

129+
// InheritProperty removes a local property value so the dataset inherits from
130+
// its parent. This is different from SetProperty with "-" which sets the literal
131+
// string "-" as the value.
132+
func (c *Client) InheritProperty(dataset, property string) error {
133+
_, err := c.Cmd.Run("inherit", property, dataset)
134+
return err
135+
}
136+
129137
// CreateSnapshot creates a ZFS snapshot.
130138
func (c *Client) CreateSnapshot(dataset, snapName string) error {
131139
full := fmt.Sprintf("%s@%s", dataset, snapName)

0 commit comments

Comments
 (0)