build / build (push) Successful in 1m4s
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>
189 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|