Files
a.gritsenkoandClaude Opus 5.5 0cdee29359
build / build (push) Successful in 1m4s
ZFS: dedup toggle per pool and dedup option for new datasets
Pool cards show the dedup ratio and can set dedup=on|off on the pool root
dataset (inherited by children); the new-dataset form gets a dedup option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:30:38 +03:00

189 lines
5.5 KiB
Go

package zfsadm
import (
"context"
"errors"
"strings"
"testing"
)
func TestValidators(t *testing.T) {
for _, s := range []string{"media", "media/video", "a.b_c:d-e", "x/y/z"} {
if err := ValidateRelName(s); err != nil {
t.Errorf("%q rejected: %v", s, err)
}
}
for _, s := range []string{"", "/media", "media/", "a//b", "..", "a/../b", ".", "-x", "a/-b", "a b", "a;b", "a/b\n", strings.Repeat("x", 201)} {
if ValidateRelName(s) == nil {
t.Errorf("%q accepted", s)
}
}
for _, s := range []string{"", "100G", "1.5T", "512M", "10k", "42"} {
if ValidateSize(s) != nil {
t.Errorf("size %q rejected", s)
}
}
for _, s := range []string{"10 G", "10GB", "G", "-1G", "1e3"} {
if ValidateSize(s) == nil {
t.Errorf("size %q accepted", s)
}
}
if ValidateCompression("gzip") == nil || ValidateCompression("lz4") != nil || ValidateCompression("") != nil {
t.Error("compression validation")
}
if ValidateDedup("verify") == nil || ValidateDedup("on") != nil || ValidateDedup("") != nil {
t.Error("dedup validation")
}
if ValidateOwner("ansible") == nil || ValidateOwner("1000:1000") != nil || ValidateOwner("") != nil {
t.Error("owner validation")
}
}
// fake records calls and answers by command prefix.
type fake struct {
calls []string
reply map[string]string
fail map[string]string
}
func (f *fake) run(_ context.Context, name string, args ...string) (string, error) {
c := name + " " + strings.Join(args, " ")
f.calls = append(f.calls, c)
for k, v := range f.fail {
if strings.HasPrefix(c, k) {
return v, errors.New(v)
}
}
for k, v := range f.reply {
if strings.HasPrefix(c, k) {
return v, nil
}
}
return "", nil
}
func TestCreate(t *testing.T) {
f := &fake{reply: map[string]string{"zfs get": "/tank/media"}}
a := New(f.run)
if err := a.Create("tank/media", CreateOpts{Compression: "lz4", Quota: "100G", Owner: "1000:1000"}); err != nil {
t.Fatal(err)
}
want := []string{
"zfs create -p -o compression=lz4 -o quota=100G tank/media",
"zfs get -H -o value mountpoint tank/media",
"chown 1000:1000 /tank/media",
}
if strings.Join(f.calls, "\n") != strings.Join(want, "\n") {
t.Errorf("calls:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n"))
}
// dedup goes between compression and quota
f = &fake{}
a = New(f.run)
if err := a.Create("tank/d", CreateOpts{Compression: "zstd", Dedup: "on", Quota: "1T"}); err != nil {
t.Fatal(err)
}
if len(f.calls) != 1 || f.calls[0] != "zfs create -p -o compression=zstd -o dedup=on -o quota=1T tank/d" {
t.Errorf("calls = %v", f.calls)
}
// no owner, inherit everything
f = &fake{}
a = New(f.run)
if err := a.Create("tank/x", CreateOpts{}); err != nil {
t.Fatal(err)
}
if len(f.calls) != 1 || f.calls[0] != "zfs create -p tank/x" {
t.Errorf("calls = %v", f.calls)
}
// invalid input never reaches the runner
f = &fake{}
a = New(f.run)
if err := a.Create("tank/../x", CreateOpts{}); err == nil || len(f.calls) != 0 {
t.Errorf("bad name: err=%v calls=%v", err, f.calls)
}
if err := a.Create("tank/x", CreateOpts{Quota: "lots"}); err == nil || len(f.calls) != 0 {
t.Errorf("bad quota: err=%v calls=%v", err, f.calls)
}
// zfs error is surfaced verbatim
f = &fake{fail: map[string]string{"zfs create": "cannot create 'tank/x': out of space"}}
a = New(f.run)
if err := a.Create("tank/x", CreateOpts{}); err == nil || !strings.Contains(err.Error(), "out of space") {
t.Errorf("err = %v", err)
}
// unmounted dataset: created but chown skipped, reported
f = &fake{reply: map[string]string{"zfs get": "none"}}
a = New(f.run)
err := a.Create("tank/x", CreateOpts{Owner: "1000:1000"})
if err == nil || !strings.Contains(err.Error(), "chown skipped") {
t.Errorf("err = %v", err)
}
for _, c := range f.calls {
if strings.HasPrefix(c, "chown") {
t.Errorf("chown must not run: %v", f.calls)
}
}
}
func TestSetDedup(t *testing.T) {
f := &fake{}
a := New(f.run)
for _, c := range [][2]string{{"tank/x", "on"}, {"tank", "verify"}, {"tank", ""}, {"-tank", "on"}, {"", "off"}} {
if err := a.SetDedup(c[0], c[1]); err == nil {
t.Errorf("%q=%q accepted", c[0], c[1])
}
}
if len(f.calls) != 0 {
t.Errorf("rejected input ran commands: %v", f.calls)
}
if err := a.SetDedup("tank", "on"); err != nil {
t.Fatal(err)
}
if err := a.SetDedup("tank", "off"); err != nil {
t.Fatal(err)
}
if strings.Join(f.calls, "|") != "zfs set dedup=on tank|zfs set dedup=off tank" {
t.Errorf("calls = %v", f.calls)
}
}
func TestDestroyEmpty(t *testing.T) {
ok := map[string]string{"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": ""}
f := &fake{reply: ok}
a := New(f.run)
if err := a.DestroyEmpty("tank/x"); err != nil {
t.Fatal(err)
}
if f.calls[len(f.calls)-1] != "zfs destroy tank/x" {
t.Errorf("calls = %v", f.calls)
}
cases := map[string]map[string]string{
"children": {"zfs list -Hp -r -t filesystem": "tank/x\t98304\ntank/x/y\t98304", "zfs list -H -t snapshot": ""},
"not empty": {"zfs list -Hp -r -t filesystem": "tank/x\t5000000", "zfs list -H -t snapshot": ""},
"snapshots": {"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": "tank/x@s1"},
}
for name, reply := range cases {
f := &fake{reply: reply}
a := New(f.run)
err := a.DestroyEmpty("tank/x")
if err == nil {
t.Errorf("%s: destroyed", name)
}
for _, c := range f.calls {
if strings.HasPrefix(c, "zfs destroy") {
t.Errorf("%s: destroy ran", name)
}
}
}
f = &fake{reply: ok}
a = New(f.run)
if err := a.DestroyEmpty("tank"); err == nil || len(f.calls) != 0 {
t.Errorf("pool root: err=%v calls=%v", err, f.calls)
}
}