mirror of
https://github.com/zrepl/zrepl.git
synced 2024-11-24 17:35:01 +01:00
add platformtest: infrastructure for ZFS compatiblity testing
This commit is contained in:
parent
07956c2299
commit
a6497b2c6e
@ -37,7 +37,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
||||
var syslogOutlets, stdoutOutlets int
|
||||
for lei, le := range in {
|
||||
|
||||
outlet, minLevel, err := parseOutlet(le)
|
||||
outlet, minLevel, err := ParseOutlet(le)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||
}
|
||||
@ -123,7 +123,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
||||
|
||||
}
|
||||
|
||||
func parseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||
func ParseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||
|
||||
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
||||
if common.Level == "" || common.Format == "" {
|
||||
|
51
platformtest/harness/harness.go
Normal file
51
platformtest/harness/harness.go
Normal file
@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/platformtest/tests"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
root := flag.String("root", "", "empty root filesystem under which we conduct the platform test")
|
||||
flag.Parse()
|
||||
if *root == "" {
|
||||
panic(*root)
|
||||
}
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
LoggingOutletCommon: config.LoggingOutletCommon{
|
||||
Level: "debug",
|
||||
Format: "human",
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
outlets.Add(outlet, level)
|
||||
logger := logger.NewLogger(outlets, 1*time.Second)
|
||||
|
||||
ctx := &platformtest.Context{
|
||||
Context: platformtest.WithLogger(context.Background(), logger),
|
||||
RootDataset: *root,
|
||||
}
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
for _, c := range tests.Cases {
|
||||
bold.Printf("BEGIN TEST CASE %s\n", c)
|
||||
c(ctx)
|
||||
bold.Printf("DONE TEST CASE %s\n", c)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
}
|
44
platformtest/harness/harness_test.go
Normal file
44
platformtest/harness/harness_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/openSUSE/umoci/blob/v0.2.1/cmd/umoci/main_test.go
|
||||
//
|
||||
// How to generate coverage:
|
||||
// go test -c -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/...
|
||||
// sudo ../logmockzfs/logzfsenv /tmp/zrepl_platform_test.log /usr/bin/zfs \
|
||||
// ./harness.test -test.coverprofile=/tmp/harness.out \
|
||||
// -test.v __DEVEL--i-heard-you-like-tests \
|
||||
// -root rpool/zreplplayground
|
||||
// go tool cover -html=/tmp/harness.out -o /tmp/harness.html
|
||||
//
|
||||
// Merge with existing coverage reports using gocovmerge:
|
||||
// https://github.com/wadey/gocovmerge
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
var (
|
||||
args []string
|
||||
run bool
|
||||
)
|
||||
|
||||
for _, arg := range os.Args {
|
||||
switch {
|
||||
case arg == "__DEVEL--i-heard-you-like-tests":
|
||||
run = true
|
||||
case strings.HasPrefix(arg, "-test"):
|
||||
case strings.HasPrefix(arg, "__DEVEL"):
|
||||
default:
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
os.Args = args
|
||||
|
||||
if run {
|
||||
main()
|
||||
}
|
||||
}
|
9
platformtest/logmockzfs/logzfsenv
Executable file
9
platformtest/logmockzfs/logzfsenv
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ue
|
||||
export ZREPL_MOCK_ZFS_COMMAND_LOG="$1"
|
||||
shift
|
||||
export ZREPL_MOCK_ZFS_PATH="$1"
|
||||
shift
|
||||
export PATH="$(dirname "${BASH_SOURCE[0]}" )":"$PATH"
|
||||
args=("$@")
|
||||
exec "${args[@]}"
|
12
platformtest/logmockzfs/zfs
Executable file
12
platformtest/logmockzfs/zfs
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
args=("$@")
|
||||
|
||||
if [ -n "$ZREPL_MOCK_ZFS_COMMAND_LOG" ]; then
|
||||
(
|
||||
flock -x 200
|
||||
jq --compact-output -n --argjson args "$(printf '%s\0' "${args[@]}" | jq -Rsc 'split("\u0000")')" '{date: now|strflocaltime("%Y-%m-%dT%H:%M:%S"), command: $args}' >> "$ZREPL_MOCK_ZFS_COMMAND_LOG"
|
||||
) 200>"$ZREPL_MOCK_ZFS_COMMAND_LOG".lock
|
||||
fi
|
||||
|
||||
exec "$ZREPL_MOCK_ZFS_PATH" "${args[@]}"
|
24
platformtest/platformtest.go
Normal file
24
platformtest/platformtest.go
Normal file
@ -0,0 +1,24 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context
|
||||
RootDataset string
|
||||
}
|
||||
|
||||
var _ assert.TestingT = (*Context)(nil)
|
||||
var _ require.TestingT = (*Context)(nil)
|
||||
|
||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||
getLog(c).Printf(format, args...)
|
||||
}
|
||||
|
||||
func (c *Context) FailNow() {
|
||||
panic(nil)
|
||||
}
|
42
platformtest/platformtest_exec.go
Normal file
42
platformtest/platformtest_exec.go
Normal file
@ -0,0 +1,42 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type ex struct {
|
||||
log Logger
|
||||
}
|
||||
|
||||
func newEx(log Logger) *ex {
|
||||
return &ex{log}
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(true, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(false, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) runNoOutput(expectSuccess bool, ctx context.Context, cmd string, args ...string) error {
|
||||
log := e.log.WithField("command", fmt.Sprintf("%q %q", cmd, args))
|
||||
log.Debug("begin executing")
|
||||
defer log.Debug("done executing")
|
||||
ecmd := exec.CommandContext(ctx, cmd, args...)
|
||||
// use circlog and capture stdout
|
||||
err := ecmd.Run()
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
if expectSuccess && err != nil {
|
||||
return fmt.Errorf("expecting no error, got error: %s\n%s", err, ee.Stderr)
|
||||
} else if !expectSuccess && err == nil {
|
||||
return fmt.Errorf("expecting error, got no error")
|
||||
}
|
||||
return nil
|
||||
}
|
24
platformtest/platformtest_logging.go
Normal file
24
platformtest/platformtest_logging.go
Normal file
@ -0,0 +1,24 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextKeyLogger contextKey = iota
|
||||
)
|
||||
|
||||
func WithLogger(ctx context.Context, logger Logger) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKeyLogger, logger)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func getLog(ctx context.Context) Logger {
|
||||
return ctx.Value(contextKeyLogger).(Logger)
|
||||
}
|
275
platformtest/platformtest_ops.go
Normal file
275
platformtest/platformtest_ops.go
Normal file
@ -0,0 +1,275 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Execer interface {
|
||||
RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
}
|
||||
|
||||
type Stmt interface {
|
||||
Run(context context.Context, e Execer) error
|
||||
}
|
||||
|
||||
type Op string
|
||||
|
||||
const (
|
||||
AssertExists Op = "!E"
|
||||
AssertNotExists Op = "!N"
|
||||
Add Op = "+"
|
||||
Del Op = "-"
|
||||
RunCmd Op = "R"
|
||||
DestroyRoot Op = "DESTROYROOT"
|
||||
CreateRoot Op = "CREATEROOT"
|
||||
)
|
||||
|
||||
type DestroyRootOp struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *DestroyRootOp) Run(ctx context.Context, e Execer) error {
|
||||
const magicName = "zreplplayground"
|
||||
// sanity check: root must contain
|
||||
if !strings.Contains(o.Path, magicName) {
|
||||
panic("sanity check failed")
|
||||
}
|
||||
// early-exit if it doesn't exist
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path); err != nil {
|
||||
getLog(ctx).WithField("root_ds", o.Path).Info("assume root ds doesn't exist")
|
||||
return nil
|
||||
}
|
||||
op, err := exec.CommandContext(ctx, "zfs", "destroy", "-nvrp", o.Path).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot clean root dataset %q: %s\n%s", o.Path, err, op)
|
||||
}
|
||||
sc := bufio.NewScanner(bytes.NewReader(op))
|
||||
for sc.Scan() {
|
||||
if !strings.Contains(sc.Text(), magicName) {
|
||||
panic("sanity check failed")
|
||||
}
|
||||
}
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", "-r", o.Path)
|
||||
}
|
||||
|
||||
type FSOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *FSOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "create", o.Path)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type SnapOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *SnapOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "snapshot", o.Path)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type RunOp struct {
|
||||
RootDS string
|
||||
Script string
|
||||
}
|
||||
|
||||
func (o *RunOp) Run(ctx context.Context, e Execer) error {
|
||||
cmd := exec.CommandContext(ctx, "/usr/bin/env", "bash", "-c", o.Script)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOTDS=%s", o.RootDS))
|
||||
log := getLog(ctx).WithField("script", o.Script)
|
||||
log.Info("start script")
|
||||
defer log.Info("script done")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("script output:\n%s", output)
|
||||
return err
|
||||
}
|
||||
|
||||
type LineError struct {
|
||||
Line string
|
||||
What string
|
||||
}
|
||||
|
||||
func (e LineError) Error() string {
|
||||
return fmt.Sprintf("%q: %s", e.Line, e.What)
|
||||
}
|
||||
|
||||
type RunKind int
|
||||
|
||||
const (
|
||||
PanicErr RunKind = 1 << iota
|
||||
RunAll
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, rk RunKind, rootds string, stmtsStr string) {
|
||||
stmt, err := parseSequence(rootds, stmtsStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
execer := newEx(getLog(ctx))
|
||||
for _, s := range stmt {
|
||||
err := s.Run(ctx, execer)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if rk == PanicErr {
|
||||
panic(err)
|
||||
} else if rk == RunAll {
|
||||
continue
|
||||
} else {
|
||||
panic(rk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNoSpace(r rune) bool {
|
||||
return !unicode.IsSpace(r)
|
||||
}
|
||||
|
||||
func splitQuotedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
begin := bytes.IndexFunc(data, isNoSpace)
|
||||
if begin == -1 {
|
||||
return len(data), nil, nil
|
||||
}
|
||||
if data[begin] == '"' {
|
||||
end := begin + 1
|
||||
for end < len(data) {
|
||||
endCandidate := bytes.Index(data[end:], []byte(`"`))
|
||||
if endCandidate == -1 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
end += endCandidate
|
||||
if data[end-1] != '\\' {
|
||||
// unescaped quote, end of this string
|
||||
// remove backslash-escapes
|
||||
withBackslash := data[begin+1 : end]
|
||||
withoutBaskslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||
return end + 1, withoutBaskslash, nil
|
||||
} else {
|
||||
// continue to next quote
|
||||
end += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endOffset := bytes.IndexFunc(data[begin:], unicode.IsSpace)
|
||||
var end int
|
||||
if endOffset == -1 {
|
||||
if !atEOF {
|
||||
return 0, nil, nil
|
||||
} else {
|
||||
end = len(data)
|
||||
}
|
||||
} else {
|
||||
end = begin + endOffset
|
||||
}
|
||||
return end, data[begin:end], nil
|
||||
}
|
||||
return 0, nil, fmt.Errorf("unexpected")
|
||||
}
|
||||
|
||||
func parseSequence(rootds, stmtsStr string) (stmts []Stmt, err error) {
|
||||
scan := bufio.NewScanner(strings.NewReader(stmtsStr))
|
||||
nextLine:
|
||||
for scan.Scan() {
|
||||
if len(bytes.TrimSpace(scan.Bytes())) == 0 {
|
||||
continue
|
||||
}
|
||||
comps := bufio.NewScanner(bytes.NewReader(scan.Bytes()))
|
||||
comps.Split(splitQuotedWords)
|
||||
|
||||
expectMoreTokens := func() error {
|
||||
if !comps.Scan() {
|
||||
return &LineError{scan.Text(), "unexpected EOL"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Op
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var op Op
|
||||
switch comps.Text() {
|
||||
|
||||
case string(RunCmd):
|
||||
script := strings.TrimPrefix(strings.TrimSpace(scan.Text()), string(RunCmd))
|
||||
stmts = append(stmts, &RunOp{RootDS: rootds, Script: script})
|
||||
continue nextLine
|
||||
|
||||
case string(DestroyRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
stmts = append(stmts, &DestroyRootOp{rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(CreateRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{Op: Add, Path: rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(Add):
|
||||
op = Add
|
||||
case string(Del):
|
||||
op = Del
|
||||
case string(AssertExists):
|
||||
op = AssertExists
|
||||
case string(AssertNotExists):
|
||||
op = AssertNotExists
|
||||
default:
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
||||
}
|
||||
|
||||
// FS / SNAP
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.ContainsAny(comps.Text(), "@") {
|
||||
stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
} else {
|
||||
stmts = append(stmts, &FSOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
}
|
||||
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
}
|
||||
return stmts, nil
|
||||
}
|
33
platformtest/platformtest_parser_test.go
Normal file
33
platformtest/platformtest_parser_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitQuotedWords(t *testing.T) {
|
||||
s := bufio.NewScanner(strings.NewReader(`
|
||||
foo "bar baz" blah "foo \"with single escape" "blah baz" "\"foo" "foo\""
|
||||
`))
|
||||
s.Split(splitQuotedWords)
|
||||
var words []string
|
||||
for s.Scan() {
|
||||
words = append(words, s.Text())
|
||||
}
|
||||
assert.Equal(
|
||||
t,
|
||||
[]string{
|
||||
"foo",
|
||||
"bar baz",
|
||||
"blah",
|
||||
"foo \"with single escape",
|
||||
"blah baz",
|
||||
"\"foo",
|
||||
"foo\"",
|
||||
},
|
||||
words)
|
||||
|
||||
}
|
54
platformtest/tests/batchDestroy.go
Normal file
54
platformtest/tests/batchDestroy.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func BatchDestroy(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
`)
|
||||
|
||||
reqs := []*zfs.DestroySnapOp{
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "3",
|
||||
},
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "2",
|
||||
},
|
||||
}
|
||||
zfs.ZFSDestroyFilesystemVersions(reqs)
|
||||
if *reqs[0].ErrOut != nil {
|
||||
panic("expecting no error")
|
||||
}
|
||||
err := (*reqs[1].ErrOut).Error()
|
||||
if !strings.Contains(err, fmt.Sprintf("%s/foo bar@2", ctx.RootDataset)) {
|
||||
panic(fmt.Sprintf("expecting error about being unable to destroy @2: %T\n%s", err, err))
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
!N "foo bar@3"
|
||||
!E "foo bar@1"
|
||||
!E "foo bar@2"
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
- "foo bar@2"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
`)
|
||||
|
||||
}
|
54
platformtest/tests/getNonexistent.go
Normal file
54
platformtest/tests/getNonexistent.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func GetNonexistent(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
`)
|
||||
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
`)
|
||||
|
||||
// test raw
|
||||
_, err := zfs.ZFSGetRawAnySource(fmt.Sprintf("%s/foo bar", ctx.RootDataset), []string{"name"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent filesystem
|
||||
nonexistent := fmt.Sprintf("%s/nonexistent filesystem", ctx.RootDataset)
|
||||
props, err := zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent snapshot
|
||||
nonexistent = fmt.Sprintf("%s/foo bar@non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok = err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
20
platformtest/tests/tests.go
Normal file
20
platformtest/tests/tests.go
Normal file
@ -0,0 +1,20 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
)
|
||||
|
||||
type Case func(*platformtest.Context)
|
||||
|
||||
func (c Case) String() string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(c).Pointer()).Name()
|
||||
}
|
||||
|
||||
var Cases = []Case{
|
||||
BatchDestroy,
|
||||
UndestroyableSnapshotParsing,
|
||||
GetNonexistent,
|
||||
}
|
40
platformtest/tests/undestroyableSnapshotParsing.go
Normal file
40
platformtest/tests/undestroyableSnapshotParsing.go
Normal file
@ -0,0 +1,40 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1 2 3"
|
||||
+ "foo bar@4 5 6"
|
||||
+ "foo bar@7 8 9"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
`)
|
||||
defer platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
DESTROYROOT
|
||||
`)
|
||||
|
||||
err := zfs.ZFSDestroy(fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
if err == nil {
|
||||
panic("expecting destroy error due to hold")
|
||||
}
|
||||
if dse, ok := err.(*zfs.DestroySnapshotsError); !ok {
|
||||
panic(fmt.Sprintf("expecting *zfs.DestroySnapshotsError, got %T\n%v\n%s", err, err, err))
|
||||
} else {
|
||||
if dse.Filesystem != fmt.Sprintf("%s/foo bar", t.RootDataset) {
|
||||
panic(dse.Filesystem)
|
||||
}
|
||||
require.Equal(t, []string{"4 5 6"}, dse.Undestroyable)
|
||||
require.Equal(t, []string{"dataset is busy"}, dse.Reason)
|
||||
}
|
||||
|
||||
}
|
@ -925,7 +925,11 @@ func ZFSGet(fs *DatasetPath, props []string) (*ZFSProperties, error) {
|
||||
return zfsGet(fs.ToString(), props, sourceAny)
|
||||
}
|
||||
|
||||
var zfsGetDatasetDoesNotExistRegexp = regexp.MustCompile(`^cannot open '([^)]+)': (dataset does not exist|no such pool or dataset)`)
|
||||
func ZFSGetRawAnySource(path string, props []string) (*ZFSProperties, error) {
|
||||
return zfsGet(path, props, sourceAny)
|
||||
}
|
||||
|
||||
var zfsGetDatasetDoesNotExistRegexp = regexp.MustCompile(`^cannot open '([^)]+)': (dataset does not exist|no such pool or dataset)`) // TODO verify this works
|
||||
|
||||
type DatasetDoesNotExist struct {
|
||||
Path string
|
||||
|
Loading…
Reference in New Issue
Block a user