kubo/test/cli/harness/harness.go
Gus Eggert 579175f81d feat: add basic CLI tests using Go Test
This is intended as a replacement for sharness. These are vanilla Go
tests which can be run in your IDE for quick iteration on end-to-end
CLI tests.

This also removes IPTB by duplicating its functionality in the test
harness. This isn't a big deal...IPTB's complexity is mostly around
the fact that its state needs to be saved to disk in between `iptb`
command invocations, and that it uses Go plugins to inject
functionality, neither of which are relevant here.

If we merge this, we'll have to live with bifurcated tests for a while
until they are all migrated. I'd recommend we self-enforce a rule
that, if we need to touch a sharness test, we migrate it and one more
test over to Go tests first. Then eventually we will have migrated
everything.
2022-12-12 09:43:09 -05:00

188 lines
4.3 KiB
Go

package harness
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
logging "github.com/ipfs/go-log/v2"
. "github.com/ipfs/kubo/test/cli/testutils"
)
// Harness tracks state for a test, such as temp dirs and IFPS nodes, and cleans them up after the test.
type Harness struct {
Dir string
IPFSBin string
Runner *Runner
NodesRoot string
Nodes Nodes
}
// TODO: use zaptest.NewLogger(t) instead
func EnableDebugLogging() {
err := logging.SetLogLevel("testharness", "DEBUG")
if err != nil {
panic(err)
}
}
// NewT constructs a harness that cleans up after the given test is done.
func NewT(t *testing.T, options ...func(h *Harness)) *Harness {
h := New(options...)
t.Cleanup(h.Cleanup)
return h
}
func New(options ...func(h *Harness)) *Harness {
h := &Harness{Runner: &Runner{Env: osEnviron()}}
// walk up to find the root dir, from which we can locate the binary
wd, err := os.Getwd()
if err != nil {
panic(err)
}
goMod := FindUp("go.mod", wd)
if goMod == "" {
panic("unable to find root dir")
}
rootDir := filepath.Dir(goMod)
h.IPFSBin = filepath.Join(rootDir, "cmd", "ipfs", "ipfs")
// setup working dir
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
log.Panicf("error creating temp dir: %s", err)
}
h.Dir = tmpDir
h.Runner.Dir = h.Dir
h.NodesRoot = filepath.Join(h.Dir, ".nodes")
// apply any customizations
// this should happen after all initialization
for _, o := range options {
o(h)
}
return h
}
func osEnviron() map[string]string {
m := map[string]string{}
for _, entry := range os.Environ() {
split := strings.Split(entry, "=")
m[split[0]] = split[1]
}
return m
}
func (h *Harness) NewNode() *Node {
nodeID := len(h.Nodes)
node := BuildNode(h.IPFSBin, h.NodesRoot, nodeID)
h.Nodes = append(h.Nodes, node)
return node
}
func (h *Harness) NewNodes(count int) Nodes {
var newNodes []*Node
for i := 0; i < count; i++ {
newNodes = append(newNodes, h.NewNode())
}
return newNodes
}
// WriteToTemp writes the given contents to a guaranteed-unique temp file, returning its path.
func (h *Harness) WriteToTemp(contents string) string {
f := h.TempFile()
_, err := f.WriteString(contents)
if err != nil {
log.Panicf("writing to temp file: %s", err.Error())
}
err = f.Close()
if err != nil {
log.Panicf("closing temp file: %s", err.Error())
}
return f.Name()
}
// TempFile creates a new unique temp file.
func (h *Harness) TempFile() *os.File {
f, err := os.CreateTemp(h.Dir, "")
if err != nil {
log.Panicf("creating temp file: %s", err.Error())
}
return f
}
// WriteFile writes a file given a filename and its contents.
// The filename should be a relative path.
func (h *Harness) WriteFile(filename, contents string) {
if filepath.IsAbs(filename) {
log.Panicf("%s must be a relative path", filename)
}
absPath := filepath.Join(h.Runner.Dir, filename)
err := os.WriteFile(absPath, []byte(contents), 0644)
if err != nil {
log.Panicf("writing '%s' ('%s'): %s", filename, absPath, err.Error())
}
}
func WaitForFile(path string, timeout time.Duration) error {
start := time.Now()
timer := time.NewTimer(timeout)
ticker := time.NewTicker(1 * time.Millisecond)
defer timer.Stop()
defer ticker.Stop()
for {
select {
case <-timer.C:
end := time.Now()
return fmt.Errorf("timeout waiting for %s after %v", path, end.Sub(start))
case <-ticker.C:
_, err := os.Stat(path)
if err == nil {
return nil
}
if errors.Is(err, os.ErrNotExist) {
continue
}
return fmt.Errorf("error waiting for %s: %w", path, err)
}
}
}
func (h *Harness) Mkdirs(paths ...string) {
for _, path := range paths {
if filepath.IsAbs(path) {
log.Panicf("%s must be a relative path when making dirs", path)
}
absPath := filepath.Join(h.Runner.Dir, path)
err := os.MkdirAll(absPath, 0777)
if err != nil {
log.Panicf("recursively making dirs under %s: %s", absPath, err)
}
}
}
func (h *Harness) Sh(expr string) RunResult {
return h.Runner.Run(RunRequest{
Path: "bash",
Args: []string{"-c", expr},
})
}
func (h *Harness) Cleanup() {
log.Debugf("cleaning up cluster")
h.Nodes.StopDaemons()
// TODO: don't do this if test fails, not sure how?
log.Debugf("removing harness dir")
err := os.RemoveAll(h.Dir)
if err != nil {
log.Panicf("removing temp dir %s: %s", h.Dir, err)
}
}