Home Download Docs Code Community
     1	/*
     2	Copyright 2013 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	package test
    18	
    19	import (
    20		"bufio"
    21		"bytes"
    22		"errors"
    23		"fmt"
    24		"io"
    25		"log"
    26		"net"
    27		"net/http"
    28		"os"
    29		"os/exec"
    30		"path/filepath"
    31		"runtime"
    32		"strings"
    33		"sync/atomic"
    34		"testing"
    35		"time"
    36	
    37		"perkeep.org/internal/osutil"
    38		"perkeep.org/pkg/blob"
    39	)
    40	
    41	// World defines an integration test world.
    42	//
    43	// It's used to run the actual Perkeep binaries (perkeepd,
    44	// pk-put, pk-get, pk, etc) together in large tests, including
    45	// building them, finding them, and wiring them up in an isolated way.
    46	type World struct {
    47		srcRoot string // typically $GOPATH[0]/src/perkeep.org
    48		config  string // server config file relative to pkg/test/testdata
    49		tempDir string
    50		gobin   string // where the World installs and finds binaries
    51	
    52		addr string // "127.0.0.1:35"
    53	
    54		server    *exec.Cmd
    55		isRunning int32 // state of the perkeepd server. Access with sync/atomic only.
    56		serverErr error
    57	}
    58	
    59	// pkSourceRoot returns the root of the source tree, or an error.
    60	func pkSourceRoot() (string, error) {
    61		root, err := osutil.GoPackagePath("perkeep.org")
    62		if err == os.ErrNotExist {
    63			return "", errors.New("directory \"perkeep.org\" not found under GOPATH/src; can't run Perkeep integration tests")
    64		}
    65		return root, nil
    66	}
    67	
    68	// NewWorld returns a new test world.
    69	// It uses the GOPATH (explicit or implicit) to find the "perkeep.org" root.
    70	func NewWorld() (*World, error) {
    71		return WorldFromConfig("server-config.json")
    72	}
    73	
    74	// WorldFromConfig returns a new test world based on the given configuration file.
    75	// This cfg is the server config relative to pkg/test/testdata.
    76	// It uses the GOPATH (explicit or implicit) to find the "perkeep.org" root.
    77	func WorldFromConfig(cfg string) (*World, error) {
    78		root, err := pkSourceRoot()
    79		if err != nil {
    80			return nil, err
    81		}
    82		return &World{
    83			srcRoot: root,
    84			config:  cfg,
    85		}, nil
    86	}
    87	
    88	func (w *World) Addr() string {
    89		return w.addr
    90	}
    91	
    92	// SourceRoot returns the root of the source tree.
    93	func (w *World) SourceRoot() string {
    94		return w.srcRoot
    95	}
    96	
    97	// Build builds the Perkeep binaries.
    98	func (w *World) Build() error {
    99		var err error
   100		w.tempDir, err = os.MkdirTemp("", "perkeep-test-")
   101		if err != nil {
   102			return err
   103		}
   104		w.gobin = filepath.Join(w.tempDir, "bin")
   105		if err := os.MkdirAll(w.gobin, 0700); err != nil {
   106			return err
   107		}
   108		// Build.
   109		{
   110			cmd := exec.Command("go", "run", "make.go",
   111				"--stampversion=false",
   112				"--targets="+strings.Join([]string{
   113					"perkeep.org/server/perkeepd",
   114					"perkeep.org/cmd/pk",
   115					"perkeep.org/cmd/pk-get",
   116					"perkeep.org/cmd/pk-put",
   117					"perkeep.org/cmd/pk-mount",
   118				}, ","))
   119			if testing.Verbose() {
   120				// TODO(mpl): do the same when -verbose with devcam test. Even better: see if testing.Verbose
   121				// can be made true if devcam test -verbose ?
   122				cmd.Args = append(cmd.Args, "-v=true")
   123			}
   124			cmd.Dir = w.srcRoot
   125			cmd.Env = append(os.Environ(), "GOBIN="+w.gobin)
   126			log.Print("Running make.go to build perkeep binaries for testing...")
   127			out, err := cmd.CombinedOutput()
   128			if err != nil {
   129				return fmt.Errorf("Error building world: %v, %s", err, string(out))
   130			}
   131			if testing.Verbose() {
   132				log.Printf("%s\n", out)
   133			}
   134			log.Print("Ran make.go.")
   135		}
   136		return nil
   137	}
   138	
   139	// Help outputs the help of perkeepd from the World.
   140	func (w *World) Help() ([]byte, error) {
   141		if err := w.Build(); err != nil {
   142			return nil, err
   143		}
   144		pkdbin := w.lookPathGobin("perkeepd")
   145		// Run perkeepd -help.
   146		cmd := exec.Command(pkdbin, "-help")
   147		return cmd.CombinedOutput()
   148	}
   149	
   150	// Start builds the Perkeep binaries and starts a server.
   151	func (w *World) Start() error {
   152		if err := w.Build(); err != nil {
   153			return err
   154		}
   155		// Start perkeepd.
   156		{
   157			pkdbin := w.lookPathGobin("perkeepd")
   158			w.server = exec.Command(
   159				pkdbin,
   160				"--openbrowser=false",
   161				"--configfile="+filepath.Join(w.srcRoot, "pkg", "test", "testdata", w.config),
   162				"--pollparent=true",
   163				"--listen=127.0.0.1:0",
   164			)
   165			var buf bytes.Buffer
   166			if testing.Verbose() {
   167				w.server.Stdout = os.Stdout
   168				w.server.Stderr = os.Stderr
   169			} else {
   170				w.server.Stdout = &buf
   171				w.server.Stderr = &buf
   172			}
   173	
   174			getPortListener, err := net.Listen("tcp", "127.0.0.1:0")
   175			if err != nil {
   176				return err
   177			}
   178			defer getPortListener.Close()
   179	
   180			w.server.Dir = w.tempDir
   181			w.server.Env = append(os.Environ(),
   182				// "CAMLI_DEBUG=1", // <-- useful for testing
   183				"CAMLI_MORE_FLAGS=1",
   184				"CAMLI_ROOT="+w.tempDir,
   185				"CAMLI_SECRET_RING="+filepath.Join(w.srcRoot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")),
   186				"CAMLI_BASE_URL=http://127.0.0.0:tbd", // filled in later
   187				"CAMLI_SET_BASE_URL_AND_SEND_ADDR_TO="+getPortListener.Addr().String(),
   188			)
   189	
   190			if err := w.server.Start(); err != nil {
   191				w.serverErr = fmt.Errorf("starting perkeepd: %v", err)
   192				return w.serverErr
   193			}
   194	
   195			atomic.StoreInt32(&w.isRunning, 1)
   196			waitc := make(chan error, 1)
   197			go func() {
   198				err := w.server.Wait()
   199				w.serverErr = fmt.Errorf("%v: %s", err, buf.String())
   200				atomic.StoreInt32(&w.isRunning, 0)
   201				waitc <- w.serverErr
   202			}()
   203			upc := make(chan bool)
   204			upErr := make(chan error, 1)
   205			go func() {
   206				c, err := getPortListener.Accept()
   207				if err != nil {
   208					upErr <- fmt.Errorf("waiting for child to report its port: %v", err)
   209					return
   210				}
   211				defer c.Close()
   212				br := bufio.NewReader(c)
   213				addr, err := br.ReadString('\n')
   214				if err != nil {
   215					upErr <- fmt.Errorf("ReadString: %v", err)
   216					return
   217				}
   218				w.addr = strings.TrimSpace(addr)
   219	
   220				for i := 0; i < 100; i++ {
   221					res, err := http.Get("http://" + w.addr)
   222					if err == nil {
   223						res.Body.Close()
   224						upc <- true
   225						return
   226					}
   227					time.Sleep(50 * time.Millisecond)
   228				}
   229				w.serverErr = errors.New(buf.String())
   230				atomic.StoreInt32(&w.isRunning, 0)
   231				upErr <- fmt.Errorf("server never became reachable: %v", w.serverErr)
   232			}()
   233	
   234			select {
   235			case <-waitc:
   236				return fmt.Errorf("server exited: %v", w.serverErr)
   237			case err := <-upErr:
   238				return err
   239			case <-upc:
   240				if err := w.Ping(); err != nil {
   241					return err
   242				}
   243				// Success.
   244			}
   245		}
   246		return nil
   247	}
   248	
   249	// Ping returns an error if the world's perkeepd is not running.
   250	func (w *World) Ping() error {
   251		if atomic.LoadInt32(&w.isRunning) != 1 {
   252			return fmt.Errorf("perkeepd not running: %v", w.serverErr)
   253		}
   254		return nil
   255	}
   256	
   257	func (w *World) Stop() {
   258		if w == nil {
   259			return
   260		}
   261		if err := w.server.Process.Kill(); err != nil {
   262			log.Fatalf("killed failed: %v", err)
   263		}
   264	
   265		if d := w.tempDir; d != "" {
   266			os.RemoveAll(d)
   267		}
   268	}
   269	
   270	func (w *World) NewPermanode(t *testing.T) blob.Ref {
   271		if err := w.Ping(); err != nil {
   272			t.Fatal(err)
   273		}
   274		out := MustRunCmd(t, w.Cmd("pk-put", "permanode"))
   275		br, ok := blob.Parse(strings.TrimSpace(out))
   276		if !ok {
   277			t.Fatalf("Expected permanode in pk-put stdout; got %q", out)
   278		}
   279		return br
   280	}
   281	
   282	func (w *World) PutFile(t *testing.T, name string) blob.Ref {
   283		out := MustRunCmd(t, w.Cmd("pk-put", "file", name))
   284		br, ok := blob.Parse(strings.TrimSpace(out))
   285		if !ok {
   286			t.Fatalf("Expected blobref in pk-put stdout; got %q", out)
   287		}
   288		return br
   289	}
   290	
   291	func (w *World) Cmd(binary string, args ...string) *exec.Cmd {
   292		return w.CmdWithEnv(binary, os.Environ(), args...)
   293	}
   294	
   295	func (w *World) CmdWithEnv(binary string, env []string, args ...string) *exec.Cmd {
   296		hasVerbose := func() bool {
   297			for _, v := range args {
   298				if v == "-verbose" || v == "--verbose" {
   299					return true
   300				}
   301			}
   302			return false
   303		}
   304		var cmd *exec.Cmd
   305		switch binary {
   306		case "pk-get", "pk-put", "pk", "pk-mount":
   307			// TODO(mpl): lift the pk-put restriction when we have a unified logging mechanism
   308			if binary == "pk-put" && !hasVerbose() {
   309				// pk-put and pk are the only ones to have a -verbose flag through cmdmain
   310				// but pk is never used. (and pk-mount does not even have a -verbose).
   311				args = append([]string{"-verbose"}, args...)
   312			}
   313			binary := w.lookPathGobin(binary)
   314	
   315			cmd = exec.Command(binary, args...)
   316			clientConfigDir := filepath.Join(w.srcRoot, "config", "dev-client-dir")
   317			cmd.Env = append(env,
   318				"CAMLI_CONFIG_DIR="+clientConfigDir,
   319				// Respected by env expansions in config/dev-client-dir/client-config.json:
   320				"CAMLI_SERVER="+w.ServerBaseURL(),
   321				"CAMLI_SECRET_RING="+w.SecretRingFile(),
   322				"CAMLI_KEYID="+w.ClientIdentity(),
   323				"CAMLI_AUTH=userpass:testuser:passTestWorld",
   324			)
   325		default:
   326			panic("Unknown binary " + binary)
   327		}
   328		return cmd
   329	}
   330	
   331	func (w *World) ServerBaseURL() string {
   332		return fmt.Sprintf("http://" + w.addr)
   333	}
   334	
   335	var theWorld *World
   336	
   337	// GetWorld returns (creating if necessary) a test singleton world.
   338	// It calls Fatal on the provided test if there are problems.
   339	func GetWorld(t *testing.T) *World {
   340		w := theWorld
   341		if w == nil {
   342			var err error
   343			w, err = NewWorld()
   344			if err != nil {
   345				t.Fatalf("Error finding test world: %v", err)
   346			}
   347			err = w.Start()
   348			if err != nil {
   349				t.Fatalf("Error starting test world: %v", err)
   350			}
   351			theWorld = w
   352		}
   353		return w
   354	}
   355	
   356	// GetWorldMaybe returns the current World. It might be nil.
   357	func GetWorldMaybe(t *testing.T) *World {
   358		return theWorld
   359	}
   360	
   361	// RunCmd runs c (which is assumed to be something short-lived, like a
   362	// pk-put or pk-get command), capturing its stdout for return, and
   363	// also capturing its stderr, just in the case of errors.
   364	// If there's an error, the return error fully describes the command and
   365	// all output.
   366	func RunCmd(c *exec.Cmd) (output string, err error) {
   367		var stdout, stderr bytes.Buffer
   368		if testing.Verbose() {
   369			c.Stderr = io.MultiWriter(os.Stderr, &stderr)
   370			c.Stdout = io.MultiWriter(os.Stdout, &stdout)
   371		} else {
   372			c.Stderr = &stderr
   373			c.Stdout = &stdout
   374		}
   375		err = c.Run()
   376		if err != nil {
   377			return "", fmt.Errorf("Error running command %+v: Stdout:\n%s\nStderr:\n%s\n", c, stdout.String(), stderr.String())
   378		}
   379		return stdout.String(), nil
   380	}
   381	
   382	// MustRunCmd wraps RunCmd, failing t if RunCmd returns an error.
   383	func MustRunCmd(t testing.TB, c *exec.Cmd) string {
   384		out, err := RunCmd(c)
   385		if err != nil {
   386			t.Fatal(err)
   387		}
   388		return out
   389	}
   390	
   391	// ClientIdentity returns the GPG identity to use in World tests, suitable
   392	// for setting in CAMLI_KEYID.
   393	func (w *World) ClientIdentity() string {
   394		return "26F5ABDA"
   395	}
   396	
   397	// SecretRingFile returns the GnuPG secret ring, suitable for setting
   398	// in CAMLI_SECRET_RING.
   399	func (w *World) SecretRingFile() string {
   400		return filepath.Join(w.srcRoot, "pkg", "jsonsign", "testdata", "test-secring.gpg")
   401	}
   402	
   403	// SearchHandlerPath returns the path to the search handler, with trailing slash.
   404	func (w *World) SearchHandlerPath() string { return "/my-search/" }
   405	
   406	// ServerBinary returns the location of the perkeepd binary running for this World.
   407	func (w *World) ServerBinary() string {
   408		return w.lookPathGobin("perkeepd")
   409	}
   410	
   411	func (w *World) lookPathGobin(binName string) string {
   412		if runtime.GOOS == "windows" && !strings.HasSuffix(binName, ".exe") {
   413			return filepath.Join(w.gobin, binName+".exe")
   414		}
   415		return filepath.Join(w.gobin, binName)
   416	}
Website layout inspired by memcached.
Content by the authors.