From 13fe0608fb7cea127a8fa9cb9083da3a98827829 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 20 Jul 2013 22:36:53 -0700 Subject: [PATCH] Start of integration tests and a library to make them easy. Change-Id: I24c55252d81d2170205f090a11a5c45473707e5d --- config/dev-client-dir/client-config.json | 2 +- pkg/test/integration/camlistore_test.go | 51 ++++++ pkg/test/testdata/server-config.json | Bin 0 -> 1469 bytes pkg/test/world.go | 224 +++++++++++++++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 pkg/test/integration/camlistore_test.go create mode 100644 pkg/test/testdata/server-config.json create mode 100644 pkg/test/world.go diff --git a/config/dev-client-dir/client-config.json b/config/dev-client-dir/client-config.json index 984acb50e..d7486054f 100644 --- a/config/dev-client-dir/client-config.json +++ b/config/dev-client-dir/client-config.json @@ -1,5 +1,5 @@ { - "server": "http://localhost:3179/", + "server": ["_env", "${CAMLI_SERVER}", "http://localhost:3179/"], "auth": ["_env", "${CAMLI_AUTH}" ], "selfPubKeyDir": ["_env", "${CAMLI_DEV_KEYBLOBS}" ], diff --git a/pkg/test/integration/camlistore_test.go b/pkg/test/integration/camlistore_test.go new file mode 100644 index 000000000..37eb8df74 --- /dev/null +++ b/pkg/test/integration/camlistore_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2013 Google Inc. + +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 integration + +import ( + "strings" + "testing" + + "camlistore.org/pkg/blobref" + "camlistore.org/pkg/test" +) + +// Test that running: +// $ camput permanode +// ... creates and uploads a permanode, and that we can camget it back. +func TestCamputPermanode(t *testing.T) { + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "permanode")) + br := blobref.Parse(strings.TrimSpace(out)) + if br == nil { + t.Fatalf("Expected permanode in stdout; got %q", out) + } + + out = test.MustRunCmd(t, w.Cmd("camget", br.String())) + mustHave := []string{ + `{"camliVersion": 1,`, + `"camliSigner": "`, + `"camliType": "permanode",`, + `random": "`, + `,"camliSig":"`, + } + for _, str := range mustHave { + if !strings.Contains(out, str) { + t.Errorf("Expected permanode response to contain %q; it didn't. Got: %s", str, out) + } + } +} diff --git a/pkg/test/testdata/server-config.json b/pkg/test/testdata/server-config.json new file mode 100644 index 0000000000000000000000000000000000000000..f495e6c88ced1c7b67899a05d1f2aee95756145a GIT binary patch literal 1469 zcmb7EQE%EX5Ps)ZL{^_Rtc;c}ls&O(50ko8f=+v=D&*J~k}ZjmZQ4eN|Gu*wLnx`b z@<4L-clLMRcb9EMmYGx(D#kULMudK#Q6&XTqd)rm3r1u(h$51+T9lx!Wt|l(#2K{# zzhTVUCT6VEekElZe$8)i?Vc>5ek8+)oNcdXKW@G+zs?rlmbkei4=$T+Om1)8Xi#gW z_0)hiHkjJz4ujuPm16p_l$@2h)W*|UE69pxuorKU9{xytcz1Ej*q*MW^mz_>sAi^C zofF#4h{&fJ)K6A{&9#KwonafFBrRn+w}&DiPBlewXvLZoD5@Z7Q44sEPqMUOSld3& zhdnT0isyS+)lltLdXNTK=>&A@dm${(oqyd82kqLPWlL=@o2x3gYs=9_Dz*ZuU|klR z9TgF9{kownjQ@pNSWWNkjmQ}FaaYm`s+c?+CC9qjUEdlS6G$p#l0`_W}bBM~jU~M+Ccz0)#XDlPF-K58c{&kj#sG zR73E{<_9mcq6FP&168+@fb4aXteI=9(-EHE-rnK(b~wN6wj;~mB!`V5RYPT6HdMf) zA0ANu&D2lhLT(iAllYWeXP{mu=O*qQ9Gv;ivrfCeFxG!qoH02Y(d<0^lqP~Dqe*&| jaG3Cm(dE@hOhAl3k1u(WB-sUI<4(VXVo?0uU^n;&YvF8S literal 0 HcmV?d00001 diff --git a/pkg/test/world.go b/pkg/test/world.go new file mode 100644 index 000000000..aee4ec074 --- /dev/null +++ b/pkg/test/world.go @@ -0,0 +1,224 @@ +/* +Copyright 2013 Google Inc. + +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 ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "camlistore.org/pkg/osutil" +) + +// World defines an integration test world. +// +// It's used to run the actual Camlistore binaries (camlistored, +// camput, camget, camtool, etc) together in large tests, including +// building them, finding them, and wiring the up in an isolated way. +type World struct { + camRoot string // typically $GOPATH[0]/src/camlistore.org + tempDir string + listener net.Listener // randomly chosen 127.0.0.1 port for the server + port int + + server *exec.Cmd + cammount *os.Process +} + +// NewWorld returns a new test world. +// It requires that GOPATH is set to find the "camlistore.org" root. +func NewWorld() (*World, error) { + if os.Getenv("GOPATH") == "" { + return nil, errors.New("GOPATH environment variable isn't set; required to run Camlistore integration tests") + } + root, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + return nil, errors.New("Directory \"camlistore.org\" not found under GOPATH/src; can't run Camlistore integration tests.") + } + if err != nil { + return nil, fmt.Errorf("Error searching for \"camlistore.org\" under GOPATH: %v", err) + } + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + + return &World{ + camRoot: root, + listener: ln, + port: ln.Addr().(*net.TCPAddr).Port, + }, nil +} + +// Start builds the Camlistore binaries and starts a server. +func (w *World) Start() error { + var err error + w.tempDir, err = ioutil.TempDir("", "camlistore-test-") + if err != nil { + return err + } + + // Build. + { + cmd := exec.Command("go", "run", "make.go") + cmd.Dir = w.camRoot + log.Printf("Running make.go to build camlistore binaries for testing...") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Error building world: %v, %s", err, string(out)) + } + log.Printf("Ran make.go.") + } + + // Start camlistored. + { + w.server = exec.Command( + filepath.Join(w.camRoot, "bin", "camlistored"), + "--configfile="+filepath.Join(w.camRoot, "pkg", "test", "testdata", "server-config.json"), + "--listen=FD:3", + ) + var buf bytes.Buffer + w.server.Stdout = &buf + w.server.Stderr = &buf + w.server.Dir = w.tempDir + w.server.Env = append(os.Environ(), + "CAMLI_ROOT="+w.tempDir, + "CAMLI_BASE_URL=http://127.0.0.1:"+strconv.Itoa(w.port), + ) + listenerFD, err := w.listener.(*net.TCPListener).File() + if err != nil { + return err + } + w.server.ExtraFiles = []*os.File{listenerFD} + if err := w.server.Start(); err != nil { + return fmt.Errorf("Starting camlistored: %v", err) + } + waitc := make(chan error, 1) + go func() { + waitc <- w.server.Wait() + }() + + reachable, tries := false, 0 + for !reachable && tries < 100 { + c, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(w.port)) + if err == nil { + reachable = true + c.Close() + break + } + + tries++ + select { + case <-time.After(50 * time.Millisecond): + case err := <-waitc: + return fmt.Errorf("server exited: %v: %s", err, buf.String()) + } + } + if !reachable { + return errors.New("server never became reachable") + } + } + return nil +} + +func (w *World) Stop() { + w.server.Process.Kill() + + if d := w.tempDir; d != "" { + os.RemoveAll(d) + } +} + +// +func (w *World) Cmd(binary string, args ...string) *exec.Cmd { + cmd := exec.Command(filepath.Join(w.camRoot, "bin", binary), args...) + switch binary { + case "camget", "camput", "camtool", "cammount": + clientConfigDir := filepath.Join(w.camRoot, "config", "dev-client-dir") + cmd.Env = append([]string{ + "CAMLI_CONFIG_DIR=" + clientConfigDir, + // Respected by env expansions in config/dev-client-dir/client-config.json: + "CAMLI_SERVER=" + w.ServerBaseURL(), + "CAMLI_SECRET_RING=" + filepath.Join(w.camRoot, "pkg", "jsonsign", "testdata", "test-secring.gpg"), + "CAMLI_KEYID=26F5ABDA", + "CAMLI_DEV_KEYBLOBS=" + filepath.Join(clientConfigDir, "keyblobs"), + "CAMLI_AUTH=userpass:testuser:passTestWorld", + }, os.Environ()...) + default: + panic("Unknown binary " + binary) + } + return cmd +} + +func (w *World) ServerBaseURL() string { + return fmt.Sprintf("http://127.0.0.1:%d", w.port) +} + +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 +} + +// RunCmd runs c (which is assumed to be something short-lived, like a +// camput or camget 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 + c.Stderr = &stderr + c.Stdout = &stdout + err = c.Run() + if err != nil { + return "", fmt.Errorf("Error running command %+v: Stdout:\n%s\nStderrr:\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.T, c *exec.Cmd) string { + out, err := RunCmd(c) + if err != nil { + t.Fatal(err) + } + return out +}