From f48c293cb0bafd436eb78a26e7bed07bd6774262 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 7 Jun 2017 14:48:15 +0200 Subject: [PATCH] common/test: Add some server init and client request utilities --- .../matrix-org/dendrite/common/test/client.go | 151 ++++++++++++++++++ .../matrix-org/dendrite/common/test/server.go | 101 ++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/github.com/matrix-org/dendrite/common/test/client.go create mode 100644 src/github.com/matrix-org/dendrite/common/test/server.go diff --git a/src/github.com/matrix-org/dendrite/common/test/client.go b/src/github.com/matrix-org/dendrite/common/test/client.go new file mode 100644 index 000000000..be61f67d5 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/test/client.go @@ -0,0 +1,151 @@ +// Copyright 2017 Vector Creations Ltd +// +// 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 ( + "fmt" + "io/ioutil" + "net/http" + "sync" + "time" + + "github.com/matrix-org/gomatrixserverlib" +) + +// Request contains the information necessary to issue a request and test its result +type Request struct { + Req *http.Request + WantedBody string + WantedStatusCode int + LastErr *LastRequestErr +} + +// LastRequestErr is a synchronized error wrapper +// Useful for obtaining the last error from a set of requests +type LastRequestErr struct { + sync.Mutex + Err error +} + +// Set sets the error +func (r *LastRequestErr) Set(err error) { + r.Lock() + defer r.Unlock() + r.Err = err +} + +// Get gets the error +func (r *LastRequestErr) Get() error { + r.Lock() + defer r.Unlock() + return r.Err +} + +// CanonicalJSONInput canonicalises a slice of JSON strings +// Useful for test input +func CanonicalJSONInput(jsonData []string) []string { + for i := range jsonData { + jsonBytes, err := gomatrixserverlib.CanonicalJSON([]byte(jsonData[i])) + if err != nil { + panic(err) + } + jsonData[i] = string(jsonBytes) + } + return jsonData +} + +// Do issues a request and checks the status code and body of the response +func (r *Request) Do() error { + client := &http.Client{ + Timeout: 5 * time.Second, + } + fmt.Println("Attempting request", r.Req) + res, err := client.Do(r.Req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != r.WantedStatusCode { + return fmt.Errorf("incorrect status code. Expected: %d Got: %d", r.WantedStatusCode, res.StatusCode) + } + + if r.WantedBody != "" { + resBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + jsonBytes, err := gomatrixserverlib.CanonicalJSON(resBytes) + if err != nil { + return err + } + if string(jsonBytes) != r.WantedBody { + return fmt.Errorf("returned wrong bytes. Expected:\n%s\n\nGot:\n%s", r.WantedBody, string(jsonBytes)) + } + } + + return nil +} + +// DoUntilSuccess blocks and repeats the same request until the response returns the desired status code and body. +// It then closes the given channel and returns. +func (r *Request) DoUntilSuccess(done chan error) { + for { + if err := r.Do(); err != nil { + r.LastErr.Set(err) + time.Sleep(1 * time.Second) // don't tightloop + continue + } + close(done) + return + } +} + +// Run repeatedly issues a request until success, error or a timeout is reached +func (r *Request) Run(label string, timeout time.Duration, serverCmdChan chan error) { + fmt.Printf("==TESTING== %v (timeout: %v)\n", label, timeout) + done := make(chan error, 1) + + // We need to wait for the server to: + // - have connected to the database + // - have created the tables + // - be listening on the given port + go r.DoUntilSuccess(done) + + // wait for one of: + // - the test to pass (done channel is closed) + // - the server to exit with an error (error sent on serverCmdChan) + // - our test timeout to expire + // We don't need to clean up since the main() function handles that in the event we panic + select { + case <-time.After(timeout): + fmt.Printf("==TESTING== %v TIMEOUT\n", label) + if reqErr := r.LastErr.Get(); reqErr != nil { + fmt.Println("Last /sync request error:") + fmt.Println(reqErr) + } + panic(fmt.Sprintf("%v server timed out", label)) + case err := <-serverCmdChan: + if err != nil { + fmt.Println("=============================================================================================") + fmt.Printf("%v server failed to run. If failing with 'pq: password authentication failed for user' try:", label) + fmt.Println(" export PGHOST=/var/run/postgresql") + fmt.Println("=============================================================================================") + panic(err) + } + case <-done: + fmt.Printf("==TESTING== %v PASSED\n", label) + } +} diff --git a/src/github.com/matrix-org/dendrite/common/test/server.go b/src/github.com/matrix-org/dendrite/common/test/server.go new file mode 100644 index 000000000..fc8124592 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/test/server.go @@ -0,0 +1,101 @@ +// Copyright 2017 Vector Creations Ltd +// +// 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 ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// Defaulting allows assignment of string variables with a fallback default value +// Useful for use with os.Getenv() for example +func Defaulting(value, defaultValue string) string { + if value == "" { + value = defaultValue + } + return value +} + +// CreateDatabase creates a new database, dropping it first if it exists +func CreateDatabase(command string, args []string, database string) error { + cmd := exec.Command(command, args...) + cmd.Stdin = strings.NewReader( + fmt.Sprintf("DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;", database, database), + ) + // Send stdout and stderr to our stderr so that we see error messages from + // the psql process + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// CreateBackgroundCommand creates an executable command +// The Cmd being executed is returned. A channel is also returned, +// which will have any termination errors sent down it, followed immediately by the channel being closed. +func CreateBackgroundCommand(command string, args []string, suffix string) (*exec.Cmd, chan error) { + cmd := exec.Command(command, args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stderr + + if err := cmd.Start(); err != nil { + panic("failed to start server: " + err.Error()) + } + cmdChan := make(chan error, 1) + go func() { + cmdChan <- cmd.Wait() + close(cmdChan) + }() + return cmd, cmdChan +} + +// StartServer creates the database and config file needed for the server to run and +// then starts the server. The Cmd being executed is returned. A channel is also returned, +// which will have any termination errors sent down it, followed immediately by the channel being closed. +func StartServer(serverType string, serverArgs []string, suffix, configFilename, configFileContents, postgresDatabase, postgresContainerName string, databases []string) (*exec.Cmd, chan error) { + if len(databases) > 0 { + var dbCmd string + var dbArgs []string + if postgresContainerName == "" { + dbCmd = "psql" + dbArgs = []string{postgresDatabase} + } else { + dbCmd = "docker" + dbArgs = []string{ + "exec", "-i", postgresContainerName, "psql", "-U", "postgres", postgresDatabase, + } + } + for _, database := range databases { + if err := CreateDatabase(dbCmd, dbArgs, database); err != nil { + panic(err) + } + } + } + + if configFilename != "" { + if err := ioutil.WriteFile(configFilename, []byte(configFileContents), 0644); err != nil { + panic(err) + } + } + + return CreateBackgroundCommand( + filepath.Join(filepath.Dir(os.Args[0]), "dendrite-"+serverType+"-server"), + serverArgs, + suffix, + ) +}