mirror of
https://github.com/zrepl/zrepl.git
synced 2024-11-21 16:03:32 +01:00
platformtest: dedicated pool per test, Makefile target, maintainer notice
fixes #216 fixes #211
This commit is contained in:
parent
215848f476
commit
58ab25919e
9
Makefile
9
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: generate build test vet cover release docs docs-clean clean format lint
|
||||
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
ARTIFACTDIR := artifacts
|
||||
@ -54,6 +54,13 @@ vet:
|
||||
GOOS=linux GOARCH=arm64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=darwin GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
$(ARTIFACTDIR)/zrepl_platformtest:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl_platformtest" ./platformtest/harness
|
||||
platformtest: $(ARTIFACTDIR)/zrepl_platformtest
|
||||
"$(ARTIFACTDIR)/zrepl_platformtest" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
||||
|
||||
$(ARTIFACTDIR):
|
||||
mkdir -p "$@"
|
||||
|
||||
|
@ -37,6 +37,7 @@ Check out the *Coding Workflow* section below for details.
|
||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||
|
||||
## Developer Documentation
|
||||
|
@ -45,6 +45,10 @@ We use the following annotations for classifying changes:
|
||||
* |bugfix| rpc goroutine leak in ``push`` mode if zfs recv fails on the ``sink`` side
|
||||
* [MAINTAINER NOTICE] Go modules for dependency management both inside and outside of GOPATH
|
||||
(``lazy.sh`` and ``Makefile`` force ``GO111MODULE=on``)
|
||||
* [MAINTAINER NOTICE] ``make platformtest`` target to check zrepl's ZFS abstractions (screen scraping, etc.).
|
||||
These tests only work on a system with ZFS installed, and must be run as root because they create a file-backed pool for each test case.
|
||||
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||
|
||||
0.1.1
|
||||
-----
|
||||
|
@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
@ -16,11 +18,14 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
root := flag.String("root", "", "empty root filesystem under which we conduct the platform test")
|
||||
flag.Parse()
|
||||
if *root == "" {
|
||||
panic(*root)
|
||||
var args struct {
|
||||
createArgs platformtest.ZpoolCreateArgs
|
||||
}
|
||||
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 100*(1<<20), "")
|
||||
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "none", "")
|
||||
flag.Parse()
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
@ -35,17 +40,40 @@ func main() {
|
||||
outlets.Add(outlet, level)
|
||||
logger := logger.NewLogger(outlets, 1*time.Second)
|
||||
|
||||
ctx := &platformtest.Context{
|
||||
Context: platformtest.WithLogger(context.Background(), logger),
|
||||
RootDataset: *root,
|
||||
if err := args.createArgs.Validate(); err != nil {
|
||||
logger.Error(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
ctx := platformtest.WithLogger(context.Background(), logger)
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
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()
|
||||
// ATTENTION future parallelism must pass c by value into closure!
|
||||
err := func() error {
|
||||
bold.Printf("BEGIN TEST CASE %s\n", c)
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create test pool")
|
||||
}
|
||||
defer func() {
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
fmt.Printf("error destroying test pool: %s", err)
|
||||
}
|
||||
}()
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
}
|
||||
c(ctx)
|
||||
bold.Printf("DONE TEST CASE %s\n", c)
|
||||
fmt.Println()
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
bold.Printf("TEST CASE FAILED WITH ERROR:\n")
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,14 +9,14 @@ import (
|
||||
// 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
|
||||
//
|
||||
/* 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 \
|
||||
-imagepath /tmp/testpool.img -poolname zreplplatformtest
|
||||
go tool cover -html=/tmp/harness.out -o /tmp/harness.html
|
||||
*/
|
||||
// Merge with existing coverage reports using gocovmerge:
|
||||
// https://github.com/wadey/gocovmerge
|
||||
|
||||
|
@ -4,13 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/zrepl/zrepl/util/circlog"
|
||||
)
|
||||
|
||||
type ex struct {
|
||||
log Logger
|
||||
}
|
||||
|
||||
func newEx(log Logger) *ex {
|
||||
func NewEx(log Logger) Execer {
|
||||
return &ex{log}
|
||||
}
|
||||
|
||||
@ -27,14 +29,15 @@ func (e *ex) runNoOutput(expectSuccess bool, ctx context.Context, cmd string, ar
|
||||
log.Debug("begin executing")
|
||||
defer log.Debug("done executing")
|
||||
ecmd := exec.CommandContext(ctx, cmd, args...)
|
||||
// use circlog and capture stdout
|
||||
buf, _ := circlog.NewCircularLog(32 << 10)
|
||||
ecmd.Stdout, ecmd.Stderr = buf, buf
|
||||
err := ecmd.Run()
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if err != nil && !ok {
|
||||
log.Printf("command output: %s", buf.String())
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
if expectSuccess && err != nil {
|
||||
return fmt.Errorf("expecting no error, got error: %s\n%s", err, ee.Stderr)
|
||||
return fmt.Errorf("expecting no error, got error: %s\n%s", err, buf.String())
|
||||
} else if !expectSuccess && err == nil {
|
||||
return fmt.Errorf("expecting error, got no error")
|
||||
}
|
||||
|
@ -37,26 +37,11 @@ type DestroyRootOp struct {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -141,7 +126,7 @@ func Run(ctx context.Context, rk RunKind, rootds string, stmtsStr string) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
execer := newEx(getLog(ctx))
|
||||
execer := NewEx(getLog(ctx))
|
||||
for _, s := range stmt {
|
||||
err := s.Run(ctx, execer)
|
||||
if err == nil {
|
||||
|
92
platformtest/platformtest_zpool.go
Normal file
92
platformtest/platformtest_zpool.go
Normal file
@ -0,0 +1,92 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type Zpool struct {
|
||||
args ZpoolCreateArgs
|
||||
}
|
||||
|
||||
type ZpoolCreateArgs struct {
|
||||
PoolName string
|
||||
ImagePath string
|
||||
ImageSize int64
|
||||
Mountpoint string
|
||||
}
|
||||
|
||||
func (a ZpoolCreateArgs) Validate() error {
|
||||
if !filepath.IsAbs(a.ImagePath) {
|
||||
return errors.Errorf("ImagePath must be absolute, got %q", a.ImagePath)
|
||||
}
|
||||
const minImageSize = 1024
|
||||
if a.ImageSize < minImageSize {
|
||||
return errors.Errorf("ImageSize must be > %v, got %v", minImageSize, a.ImageSize)
|
||||
}
|
||||
if a.Mountpoint != "none" {
|
||||
return errors.Errorf("Mountpoint must be \"none\"")
|
||||
}
|
||||
if a.PoolName == "" {
|
||||
return errors.Errorf("PoolName must not be emtpy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (*Zpool, error) {
|
||||
if err := args.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create args validation error")
|
||||
}
|
||||
|
||||
// export pool if it already exists (idempotence)
|
||||
if _, err := zfs.ZFSGetRawAnySource(args.PoolName, []string{"name"}); err != nil {
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||
// we'll create it shortly
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "cannot determine whether test pool %q exists", args.PoolName)
|
||||
}
|
||||
} else {
|
||||
// exists, export it, OpenFile will destroy it
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", args.PoolName); err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot destroy test pool %q", args.PoolName)
|
||||
}
|
||||
}
|
||||
|
||||
// idempotently (re)create the pool image
|
||||
image, err := os.OpenFile(args.ImagePath, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create image file")
|
||||
}
|
||||
defer image.Close()
|
||||
if err := image.Truncate(args.ImageSize); err != nil {
|
||||
return nil, errors.Wrap(err, "create image: truncate")
|
||||
}
|
||||
image.Close()
|
||||
|
||||
// create the pool
|
||||
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-O", "mountpoint=none", args.PoolName, args.ImagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create")
|
||||
}
|
||||
|
||||
return &Zpool{args}, nil
|
||||
}
|
||||
|
||||
func (p *Zpool) Name() string { return p.args.PoolName }
|
||||
|
||||
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
||||
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName); err != nil {
|
||||
return errors.Wrapf(err, "export pool %q", p.args.PoolName)
|
||||
}
|
||||
|
||||
if err := os.Remove(p.args.ImagePath); err != nil {
|
||||
return errors.Wrapf(err, "remove pool image")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user