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
|
var syslogOutlets, stdoutOutlets int
|
||||||
for lei, le := range in {
|
for lei, le := range in {
|
||||||
|
|
||||||
outlet, minLevel, err := parseOutlet(le)
|
outlet, minLevel, err := ParseOutlet(le)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
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) {
|
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
||||||
if common.Level == "" || common.Format == "" {
|
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)
|
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 {
|
type DatasetDoesNotExist struct {
|
||||||
Path string
|
Path string
|
||||||
|
Loading…
Reference in New Issue
Block a user