Skip to content

Commit cc89ac0

Browse files
kvapsclaude
andcommitted
test(zfs): integration smoke against a real zpool (opt-in)
`BLOCKSTOR_ZFS_POOL=blockstor-test go test -v ./pkg/storage/zfs/...` walks the Provider through CreateVolume → VolumeStatus → CreateSnapshot → DeleteSnapshot → DeleteVolume against a real `zfs` binary. Skipped when the env var is unset or zfs isn't on PATH, so the unit-test path stays unchanged. PoolStatus separately validates that free/total bytes look sensible on the pre-created pool. Together they exercise the RealExec/zfs/zpool wire format end-to-end without spinning up the satellite. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 819f669 commit cc89ac0

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright 2026 Cozystack contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package zfs_test
18+
19+
import (
20+
"os"
21+
"os/exec"
22+
"strings"
23+
"testing"
24+
25+
"github.com/cozystack/blockstor/pkg/storage"
26+
"github.com/cozystack/blockstor/pkg/storage/zfs"
27+
)
28+
29+
// TestZFSAgainstRealPool runs the Provider through CreateVolume →
30+
// VolumeStatus → CreateSnapshot → DeleteSnapshot → DeleteVolume on
31+
// a real `zfs` binary against a pre-created pool whose name is
32+
// passed via the `BLOCKSTOR_ZFS_POOL` env var.
33+
//
34+
// Skipped if either:
35+
// - `zfs` is not on PATH (developer laptop without ZFS)
36+
// - BLOCKSTOR_ZFS_POOL is unset (CI/regression doesn't auto-touch
37+
// a real pool — the dev stand opts in by setting it)
38+
//
39+
// On the dev stand: `sudo zpool create blockstor-test /dev/loopN` →
40+
// `BLOCKSTOR_ZFS_POOL=blockstor-test go test -run TestZFSAgainst -v`.
41+
func TestZFSAgainstRealPool(t *testing.T) {
42+
if _, err := exec.LookPath("zfs"); err != nil {
43+
t.Skip("zfs binary not on PATH; skipping integration smoke")
44+
}
45+
46+
pool := requireEnv(t, "BLOCKSTOR_ZFS_POOL")
47+
48+
provider := zfs.NewProvider(zfs.Config{Pool: pool}, storage.RealExec{})
49+
ctx := t.Context()
50+
51+
vol := storage.Volume{
52+
ResourceName: "blockstor-it",
53+
VolumeNumber: 0,
54+
SizeKib: 8 * 1024, // 8 MiB — keep it tiny so a small pool fits
55+
}
56+
57+
// Cleanup whatever a prior aborted run left behind.
58+
t.Cleanup(func() {
59+
_ = provider.DeleteVolume(t.Context(), vol)
60+
})
61+
62+
err := provider.CreateVolume(ctx, vol)
63+
if err != nil {
64+
t.Fatalf("CreateVolume: %v", err)
65+
}
66+
67+
// Idempotent — second CreateVolume must not blow up on the
68+
// already-existing dataset.
69+
err = provider.CreateVolume(ctx, vol)
70+
if err != nil {
71+
t.Fatalf("CreateVolume (idempotent): %v", err)
72+
}
73+
74+
st, err := provider.VolumeStatus(ctx, vol)
75+
if err != nil {
76+
t.Fatalf("VolumeStatus: %v", err)
77+
}
78+
79+
if st.State != "PROVISIONED" {
80+
t.Errorf("State: got %q, want PROVISIONED", st.State)
81+
}
82+
83+
if !strings.HasPrefix(st.DevicePath, "/dev/zvol/"+pool+"/") {
84+
t.Errorf("DevicePath: got %q, want /dev/zvol/%s/...", st.DevicePath, pool)
85+
}
86+
87+
if st.UsableKib < vol.SizeKib {
88+
t.Errorf("UsableKib: got %d, want >= %d", st.UsableKib, vol.SizeKib)
89+
}
90+
91+
// Snapshot round-trip.
92+
snap := storage.Snapshot{ResourceName: vol.ResourceName, SnapshotName: "snap-1"}
93+
94+
err = provider.CreateSnapshot(ctx, snap)
95+
if err != nil {
96+
t.Fatalf("CreateSnapshot: %v", err)
97+
}
98+
99+
err = provider.DeleteSnapshot(ctx, snap)
100+
if err != nil {
101+
t.Fatalf("DeleteSnapshot: %v", err)
102+
}
103+
104+
err = provider.DeleteVolume(ctx, vol)
105+
if err != nil {
106+
t.Fatalf("DeleteVolume: %v", err)
107+
}
108+
109+
// After delete, status reports NOT_PROVISIONED.
110+
st, err = provider.VolumeStatus(ctx, vol)
111+
if err != nil {
112+
t.Fatalf("VolumeStatus (post-delete): %v", err)
113+
}
114+
115+
if st.State != "NOT_PROVISIONED" {
116+
t.Errorf("post-delete State: got %q, want NOT_PROVISIONED", st.State)
117+
}
118+
}
119+
120+
// TestZFSPoolStatusAgainstRealPool just walks PoolStatus once and
121+
// verifies the numbers look sensible.
122+
func TestZFSPoolStatusAgainstRealPool(t *testing.T) {
123+
if _, err := exec.LookPath("zpool"); err != nil {
124+
t.Skip("zpool binary not on PATH; skipping integration smoke")
125+
}
126+
127+
pool := requireEnv(t, "BLOCKSTOR_ZFS_POOL")
128+
129+
provider := zfs.NewProvider(zfs.Config{Pool: pool}, storage.RealExec{})
130+
131+
ps, err := provider.PoolStatus(t.Context())
132+
if err != nil {
133+
t.Fatalf("PoolStatus: %v", err)
134+
}
135+
136+
if ps.TotalCapacityKib <= 0 {
137+
t.Errorf("TotalCapacityKib: got %d, want > 0", ps.TotalCapacityKib)
138+
}
139+
140+
if ps.FreeCapacityKib < 0 || ps.FreeCapacityKib > ps.TotalCapacityKib {
141+
t.Errorf("FreeCapacityKib: got %d, want 0..%d", ps.FreeCapacityKib, ps.TotalCapacityKib)
142+
}
143+
144+
if !ps.SupportsSnapshots {
145+
t.Errorf("SupportsSnapshots: got false, want true (ZFS always supports snapshots)")
146+
}
147+
}
148+
149+
// requireEnv reads the env var or skips the test (rather than fails)
150+
// when missing — these are opt-in integration tests.
151+
func requireEnv(t *testing.T, key string) string {
152+
t.Helper()
153+
154+
v := os.Getenv(key)
155+
if v == "" {
156+
t.Skipf("%s not set; skipping integration smoke", key)
157+
}
158+
159+
return v
160+
}

0 commit comments

Comments
 (0)