mirror of https://github.com/perkeep/perkeep.git
408 lines
11 KiB
Go
408 lines
11 KiB
Go
/*
|
|
Copyright 2013 The Perkeep Authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"perkeep.org/internal/osutil"
|
|
"perkeep.org/pkg/blob"
|
|
)
|
|
|
|
// World defines an integration test world.
|
|
//
|
|
// It's used to run the actual Perkeep binaries (perkeepd,
|
|
// pk-put, pk-get, pk, etc) together in large tests, including
|
|
// building them, finding them, and wiring them up in an isolated way.
|
|
type World struct {
|
|
srcRoot string // typically $GOPATH[0]/src/perkeep.org or just the root dir where go.mod is
|
|
config string // server config file relative to pkg/test/testdata
|
|
tempDir string
|
|
gobin string // where the World installs and finds binaries
|
|
|
|
addr string // "127.0.0.1:35"
|
|
|
|
server *exec.Cmd
|
|
isRunning int32 // state of the perkeepd server. Access with sync/atomic only.
|
|
serverErr error
|
|
}
|
|
|
|
// NewWorld returns a new test world.
|
|
// It uses the GOPATH (explicit or implicit) to find the "perkeep.org" root.
|
|
func NewWorld() (*World, error) {
|
|
return WorldFromConfig("server-config.json")
|
|
}
|
|
|
|
// WorldFromConfig returns a new test world based on the given configuration file.
|
|
// This cfg is the server config relative to pkg/test/testdata.
|
|
// It uses the GOPATH (explicit or implicit) to find the "perkeep.org" root.
|
|
func WorldFromConfig(cfg string) (*World, error) {
|
|
root, err := osutil.PkSourceRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &World{
|
|
srcRoot: root,
|
|
config: cfg,
|
|
}, nil
|
|
}
|
|
|
|
func (w *World) Addr() string {
|
|
return w.addr
|
|
}
|
|
|
|
// SourceRoot returns the root of the source tree.
|
|
func (w *World) SourceRoot() string {
|
|
return w.srcRoot
|
|
}
|
|
|
|
// Build builds the Perkeep binaries.
|
|
func (w *World) Build() error {
|
|
var err error
|
|
w.tempDir, err = os.MkdirTemp("", "perkeep-test-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.gobin = filepath.Join(w.tempDir, "bin")
|
|
if err := os.MkdirAll(w.gobin, 0700); err != nil {
|
|
return err
|
|
}
|
|
// Build.
|
|
{
|
|
cmd := exec.Command("go", "run", "make.go",
|
|
"--stampversion=false",
|
|
"--targets="+strings.Join([]string{
|
|
"perkeep.org/server/perkeepd",
|
|
"perkeep.org/cmd/pk",
|
|
"perkeep.org/cmd/pk-get",
|
|
"perkeep.org/cmd/pk-put",
|
|
"perkeep.org/cmd/pk-mount",
|
|
}, ","))
|
|
if testing.Verbose() {
|
|
// TODO(mpl): do the same when -verbose with devcam test. Even better: see if testing.Verbose
|
|
// can be made true if devcam test -verbose ?
|
|
cmd.Args = append(cmd.Args, "-v=true")
|
|
}
|
|
cmd.Dir = w.srcRoot
|
|
cmd.Env = append(os.Environ(), "GOBIN="+w.gobin)
|
|
log.Print("Running make.go to build perkeep binaries for testing...")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("Error building world: %v, %s", err, string(out))
|
|
}
|
|
if testing.Verbose() {
|
|
log.Printf("%s\n", out)
|
|
}
|
|
log.Print("Ran make.go.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Help outputs the help of perkeepd from the World.
|
|
func (w *World) Help() ([]byte, error) {
|
|
if err := w.Build(); err != nil {
|
|
return nil, err
|
|
}
|
|
pkdbin := w.lookPathGobin("perkeepd")
|
|
// Run perkeepd -help.
|
|
cmd := exec.Command(pkdbin, "-help")
|
|
return cmd.CombinedOutput()
|
|
}
|
|
|
|
// Start builds the Perkeep binaries and starts a server.
|
|
func (w *World) Start() error {
|
|
if err := w.Build(); err != nil {
|
|
return err
|
|
}
|
|
// Start perkeepd.
|
|
{
|
|
pkdbin := w.lookPathGobin("perkeepd")
|
|
w.server = exec.Command(
|
|
pkdbin,
|
|
"--openbrowser=false",
|
|
"--configfile="+filepath.Join(w.srcRoot, "pkg", "test", "testdata", w.config),
|
|
"--pollparent=true",
|
|
"--listen=127.0.0.1:0",
|
|
)
|
|
var buf bytes.Buffer
|
|
if testing.Verbose() {
|
|
w.server.Stdout = os.Stdout
|
|
w.server.Stderr = os.Stderr
|
|
} else {
|
|
w.server.Stdout = &buf
|
|
w.server.Stderr = &buf
|
|
}
|
|
|
|
getPortListener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer getPortListener.Close()
|
|
|
|
w.server.Dir = w.tempDir
|
|
w.server.Env = append(os.Environ(),
|
|
// "CAMLI_DEBUG=1", // <-- useful for testing
|
|
"CAMLI_MORE_FLAGS=1",
|
|
"CAMLI_ROOT="+w.tempDir,
|
|
"CAMLI_SECRET_RING="+filepath.Join(w.srcRoot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")),
|
|
"CAMLI_BASE_URL=http://127.0.0.0:tbd", // filled in later
|
|
"CAMLI_SET_BASE_URL_AND_SEND_ADDR_TO="+getPortListener.Addr().String(),
|
|
)
|
|
|
|
if err := w.server.Start(); err != nil {
|
|
w.serverErr = fmt.Errorf("starting perkeepd: %v", err)
|
|
return w.serverErr
|
|
}
|
|
|
|
atomic.StoreInt32(&w.isRunning, 1)
|
|
waitc := make(chan error, 1)
|
|
go func() {
|
|
err := w.server.Wait()
|
|
w.serverErr = fmt.Errorf("%v: %s", err, buf.String())
|
|
atomic.StoreInt32(&w.isRunning, 0)
|
|
waitc <- w.serverErr
|
|
}()
|
|
upc := make(chan bool)
|
|
upErr := make(chan error, 1)
|
|
go func() {
|
|
c, err := getPortListener.Accept()
|
|
if err != nil {
|
|
upErr <- fmt.Errorf("waiting for child to report its port: %v", err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
br := bufio.NewReader(c)
|
|
addr, err := br.ReadString('\n')
|
|
if err != nil {
|
|
upErr <- fmt.Errorf("ReadString: %v", err)
|
|
return
|
|
}
|
|
w.addr = strings.TrimSpace(addr)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
res, err := http.Get("http://" + w.addr)
|
|
if err == nil {
|
|
res.Body.Close()
|
|
upc <- true
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
w.serverErr = errors.New(buf.String())
|
|
atomic.StoreInt32(&w.isRunning, 0)
|
|
upErr <- fmt.Errorf("server never became reachable: %v", w.serverErr)
|
|
}()
|
|
|
|
select {
|
|
case <-waitc:
|
|
return fmt.Errorf("server exited: %v", w.serverErr)
|
|
case err := <-upErr:
|
|
return err
|
|
case <-upc:
|
|
if err := w.Ping(); err != nil {
|
|
return err
|
|
}
|
|
// Success.
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ping returns an error if the world's perkeepd is not running.
|
|
func (w *World) Ping() error {
|
|
if atomic.LoadInt32(&w.isRunning) != 1 {
|
|
return fmt.Errorf("perkeepd not running: %v", w.serverErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *World) Stop() {
|
|
if w == nil {
|
|
return
|
|
}
|
|
if err := w.server.Process.Kill(); err != nil {
|
|
log.Fatalf("killed failed: %v", err)
|
|
}
|
|
|
|
if d := w.tempDir; d != "" {
|
|
os.RemoveAll(d)
|
|
}
|
|
}
|
|
|
|
func (w *World) NewPermanode(t *testing.T) blob.Ref {
|
|
if err := w.Ping(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := MustRunCmd(t, w.Cmd("pk-put", "permanode"))
|
|
br, ok := blob.Parse(strings.TrimSpace(out))
|
|
if !ok {
|
|
t.Fatalf("Expected permanode in pk-put stdout; got %q", out)
|
|
}
|
|
return br
|
|
}
|
|
|
|
func (w *World) PutFile(t *testing.T, name string) blob.Ref {
|
|
out := MustRunCmd(t, w.Cmd("pk-put", "file", name))
|
|
br, ok := blob.Parse(strings.TrimSpace(out))
|
|
if !ok {
|
|
t.Fatalf("Expected blobref in pk-put stdout; got %q", out)
|
|
}
|
|
return br
|
|
}
|
|
|
|
func (w *World) Cmd(binary string, args ...string) *exec.Cmd {
|
|
return w.CmdWithEnv(binary, os.Environ(), args...)
|
|
}
|
|
|
|
func (w *World) CmdWithEnv(binary string, env []string, args ...string) *exec.Cmd {
|
|
hasVerbose := func() bool {
|
|
for _, v := range args {
|
|
if v == "-verbose" || v == "--verbose" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
var cmd *exec.Cmd
|
|
switch binary {
|
|
case "pk-get", "pk-put", "pk", "pk-mount":
|
|
// TODO(mpl): lift the pk-put restriction when we have a unified logging mechanism
|
|
if binary == "pk-put" && !hasVerbose() {
|
|
// pk-put and pk are the only ones to have a -verbose flag through cmdmain
|
|
// but pk is never used. (and pk-mount does not even have a -verbose).
|
|
args = append([]string{"-verbose"}, args...)
|
|
}
|
|
binary := w.lookPathGobin(binary)
|
|
|
|
cmd = exec.Command(binary, args...)
|
|
clientConfigDir := filepath.Join(w.srcRoot, "config", "dev-client-dir")
|
|
cmd.Env = append(env,
|
|
"CAMLI_CONFIG_DIR="+clientConfigDir,
|
|
// Respected by env expansions in config/dev-client-dir/client-config.json:
|
|
"CAMLI_SERVER="+w.ServerBaseURL(),
|
|
"CAMLI_SECRET_RING="+w.SecretRingFile(),
|
|
"CAMLI_KEYID="+w.ClientIdentity(),
|
|
"CAMLI_AUTH=userpass:testuser:passTestWorld",
|
|
)
|
|
default:
|
|
panic("Unknown binary " + binary)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func (w *World) ServerBaseURL() string {
|
|
return fmt.Sprintf("http://" + w.addr)
|
|
}
|
|
|
|
var theWorld *World
|
|
|
|
// GetWorld returns (creating if necessary) a test singleton world.
|
|
// It calls Fatal on the provided test if there are problems.
|
|
func GetWorld(t *testing.T) *World {
|
|
w := theWorld
|
|
if w == nil {
|
|
var err error
|
|
w, err = NewWorld()
|
|
if err != nil {
|
|
t.Fatalf("Error finding test world: %v", err)
|
|
}
|
|
err = w.Start()
|
|
if err != nil {
|
|
t.Fatalf("Error starting test world: %v", err)
|
|
}
|
|
theWorld = w
|
|
}
|
|
return w
|
|
}
|
|
|
|
// GetWorldMaybe returns the current World. It might be nil.
|
|
func GetWorldMaybe(t *testing.T) *World {
|
|
return theWorld
|
|
}
|
|
|
|
// RunCmd runs c (which is assumed to be something short-lived, like a
|
|
// pk-put or pk-get command), capturing its stdout for return, and
|
|
// also capturing its stderr, just in the case of errors.
|
|
// If there's an error, the return error fully describes the command and
|
|
// all output.
|
|
func RunCmd(c *exec.Cmd) (output string, err error) {
|
|
var stdout, stderr bytes.Buffer
|
|
if testing.Verbose() {
|
|
c.Stderr = io.MultiWriter(os.Stderr, &stderr)
|
|
c.Stdout = io.MultiWriter(os.Stdout, &stdout)
|
|
} else {
|
|
c.Stderr = &stderr
|
|
c.Stdout = &stdout
|
|
}
|
|
err = c.Run()
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error running command %+v: Stdout:\n%s\nStderr:\n%s\n", c, stdout.String(), stderr.String())
|
|
}
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// MustRunCmd wraps RunCmd, failing t if RunCmd returns an error.
|
|
func MustRunCmd(t testing.TB, c *exec.Cmd) string {
|
|
out, err := RunCmd(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ClientIdentity returns the GPG identity to use in World tests, suitable
|
|
// for setting in CAMLI_KEYID.
|
|
func (w *World) ClientIdentity() string {
|
|
return "26F5ABDA"
|
|
}
|
|
|
|
// SecretRingFile returns the GnuPG secret ring, suitable for setting
|
|
// in CAMLI_SECRET_RING.
|
|
func (w *World) SecretRingFile() string {
|
|
return filepath.Join(w.srcRoot, "pkg", "jsonsign", "testdata", "test-secring.gpg")
|
|
}
|
|
|
|
// SearchHandlerPath returns the path to the search handler, with trailing slash.
|
|
func (w *World) SearchHandlerPath() string { return "/my-search/" }
|
|
|
|
// ServerBinary returns the location of the perkeepd binary running for this World.
|
|
func (w *World) ServerBinary() string {
|
|
return w.lookPathGobin("perkeepd")
|
|
}
|
|
|
|
func (w *World) lookPathGobin(binName string) string {
|
|
if runtime.GOOS == "windows" && !strings.HasSuffix(binName, ".exe") {
|
|
return filepath.Join(w.gobin, binName+".exe")
|
|
}
|
|
return filepath.Join(w.gobin, binName)
|
|
}
|