From 6fce972508c119ae6d5de354cd79fbfe3bcb943c Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Thu, 7 Feb 2019 13:28:42 +0100 Subject: [PATCH 1/7] implements a livetest function #47 --- Makefile | 1 + testing/livetest/doc.go | 23 ++++++ testing/livetest/init.go | 48 ++++++++++++ testing/livetest/livetest.go | 106 +++++++++++++++++++++++++++ testing/livetest/livetest_test.go | 57 +++++++++++++++ testing/livetest/test_proxy.go | 117 ++++++++++++++++++++++++++++++ tools/testserver/main.go | 18 +++++ 7 files changed, 370 insertions(+) create mode 100644 testing/livetest/doc.go create mode 100644 testing/livetest/init.go create mode 100644 testing/livetest/livetest.go create mode 100644 testing/livetest/livetest_test.go create mode 100644 testing/livetest/test_proxy.go diff --git a/Makefile b/Makefile index a31ba7d02..930c1ba1b 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ testserver: POSTGRES_USER=testserveruser \ POSTGRES_DB=testserver \ POSTGRES_PASSWORD=pace1234! \ + PACE_LIVETEST_INTERVAL=1m \ go run ./tools/testserver/main.go docker.all: docker.jaeger docker.postgres.setup docker.redis diff --git a/testing/livetest/doc.go b/testing/livetest/doc.go new file mode 100644 index 000000000..dd471a2ce --- /dev/null +++ b/testing/livetest/doc.go @@ -0,0 +1,23 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/01 by Vincent Landgraf + +// Package livetest implements a set of helpers that ease writing of a +// sidecar that tests the functions of a service. +// +// Assuring the functional correctness of a service is an important +// task for a production grade system. This package aims to provide +// helpers that allow a go test like experience for building functional +// health tests in production. +// +// Test functions need to be written similarly to the regular go test +// function format. Only difference is the use of the testing.TB +// interface. +// +// If a test failed, all other tests are still executed. So tests +// should not build on each other. Sub tests should be used for that +// purpose. +// +// The result for the tests is exposed via prometheus metrics. +// +// The interval is configured using PACE_LIVETEST_INTERVAL (duration format). +package livetest diff --git a/testing/livetest/init.go b/testing/livetest/init.go new file mode 100644 index 000000000..e4b59df49 --- /dev/null +++ b/testing/livetest/init.go @@ -0,0 +1,48 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/04 by Vincent Landgraf + +package livetest + +import ( + "log" + "time" + + "github.com/caarlos0/env" + "github.com/prometheus/client_golang/prometheus" +) + +type config struct { + Interval time.Duration `env:"PACE_LIVETEST_INTERVAL" envDefault:"1h"` + ServiceName string `env:"JAEGER_SERVICE_NAME" envDefault:"go-microservice"` +} + +var ( + paceLivetestTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "pace_livetest_total", + Help: "Collects stats about the number of live tests made", + }, + []string{"service", "result"}, + ) + paceLivetestDurationSeconds = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "pace_livetest_duration_seconds", + Help: "Collect performance metrics for each live test", + Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 10, 60}, + }, + []string{"service"}, + ) +) + +var cfg config + +func init() { + prometheus.MustRegister(paceLivetestTotal) + prometheus.MustRegister(paceLivetestDurationSeconds) + + // parse log config + err := env.Parse(&cfg) + if err != nil { + log.Fatalf("Failed to parse livetest environment: %v", err) + } +} diff --git a/testing/livetest/livetest.go b/testing/livetest/livetest.go new file mode 100644 index 000000000..5b5cdf36b --- /dev/null +++ b/testing/livetest/livetest.go @@ -0,0 +1,106 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/01 by Vincent Landgraf + +package livetest + +import ( + "context" + "fmt" + "runtime/debug" + "strings" + "time" + + opentracing "github.com/opentracing/opentracing-go" + "lab.jamit.de/pace/go-microservice/maintenance/log" +) + +// TestFunc represents a single test (possibly with sub tests) +type TestFunc func(t *T) + +// Test executes the passed tests in the given order (array order). +// The tests are executed in the configured interval until the given +// context is done. +func Test(ctx context.Context, tests []TestFunc) error { + t := time.NewTicker(cfg.Interval) + + // run test at least once + testRun(ctx, tests) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + testRun(ctx, tests) + } + } +} + +func testRun(ctx context.Context, tests []TestFunc) { + var err error + + // setup logger in context + testrun := time.Now().Unix() + + for i, test := range tests { + logger := log.Ctx(log.WithContext(ctx)).With(). + Int64("livetest", testrun). + Int("test", i+1).Logger() + ctx = logger.WithContext(ctx) + + err := executeTest(ctx, test, fmt.Sprintf("test-%d", i+1)) + if err != nil { + break + } + } + + if err != nil { + log.Errorf("Failed to run tests: %v", err) + } +} + +func executeTest(ctx context.Context, t TestFunc, name string) error { + // setup tracing + span, ctx := opentracing.StartSpanFromContext(ctx, "Livetest") + defer span.Finish() + logger := log.Ctx(ctx) + + proxy := NewTestProxy(ctx, name) + startTime := time.Now() + func() { + defer func() { + err := recover() + if err != SkipNow || err != FailNow { + lines := strings.Split(string(debug.Stack()[:]), "\n") + for _, line := range lines { + proxy.Error(line) + } + proxy.Fail() + } + }() + + t(proxy) + }() + duration := float64(time.Since(startTime)) / float64(time.Second) + proxy.okIfNoSkipFail() + paceLivetestDurationSeconds.WithLabelValues(cfg.ServiceName).Observe(duration) + + switch proxy.State { + case StateFailed: + logger.Warn().Msg("Test failed!") + span.LogEvent("Test failed!") + paceLivetestTotal.WithLabelValues(cfg.ServiceName, "failed").Add(1) + case StateOK: + logger.Info().Msg("Test succeeded!") + span.LogEvent("Test succeeded!") + paceLivetestTotal.WithLabelValues(cfg.ServiceName, "succeeded").Add(1) + case StateSkipped: + logger.Info().Msg("Test skipped!") + span.LogEvent("Test skipped!") + paceLivetestTotal.WithLabelValues(cfg.ServiceName, "skipped").Add(1) + default: + panic(fmt.Errorf("Invalid state: %v", proxy.State)) + } + + return nil +} diff --git a/testing/livetest/livetest_test.go b/testing/livetest/livetest_test.go new file mode 100644 index 000000000..8e95617f1 --- /dev/null +++ b/testing/livetest/livetest_test.go @@ -0,0 +1,57 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/01 by Vincent Landgraf + +package livetest_test + +import ( + "context" + "log" + "time" + + "lab.jamit.de/pace/go-microservice/testing/livetest" +) + +func ExampleTest() { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) + defer cancel() + + err := livetest.Test(ctx, []livetest.TestFunc{ + func(t *livetest.T) { + t.Logf("Executed test no %d", 1) + }, + func(t *livetest.T) { + t.Log("Executed test no 2") + }, + func(t *livetest.T) { + t.Fatal("Fail test no 3") + }, + func(t *livetest.T) { + t.Fatalf("Fail test no %d", 4) + }, + func(t *livetest.T) { + t.Skip("Skipping test no 5") + }, + func(t *livetest.T) { + t.Skipf("Skipping test no %d", 5) + }, + func(t *livetest.T) { + t.SkipNow() + }, + func(t *livetest.T) { + t.Fail() + }, + func(t *livetest.T) { + t.FailNow() + }, + func(t *livetest.T) { + t.Error("Some") + }, + func(t *livetest.T) { + t.Errorf("formatted") + }, + }) + if err != context.DeadlineExceeded { + log.Fatal(err) + } + // Output: +} diff --git a/testing/livetest/test_proxy.go b/testing/livetest/test_proxy.go new file mode 100644 index 000000000..2b93a47e7 --- /dev/null +++ b/testing/livetest/test_proxy.go @@ -0,0 +1,117 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/06 by Vincent Landgraf + +package livetest + +import ( + "context" + "errors" + "fmt" + + "lab.jamit.de/pace/go-microservice/maintenance/log" +) + +// SkipNow is used as a panic if SkipNow is called on the test +var SkipNow = errors.New("skipped test") + +// FailNow is used as a panic if FailNow is called on the test +var FailNow = errors.New("failed test") + +type TestState string + +var ( + StateRunning TestState = "running" + StateOK TestState = "ok" + StateFailed TestState = "failed" + StateSkipped TestState = "skipped" +) + +type T struct { + name string + ctx context.Context + State TestState +} + +func NewTestProxy(ctx context.Context, name string) *T { + return &T{name: name, ctx: ctx, State: StateRunning} +} + +func (t *T) Error(args ...interface{}) { + log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) +} + +func (t *T) Errorf(format string, args ...interface{}) { + log.Ctx(t.ctx).Error().Msgf(format, args...) +} + +func (t *T) Fail() { + log.Ctx(t.ctx).Info().Msg("Fail...") + if t.State == StateRunning { + t.State = StateFailed + } +} + +func (t *T) FailNow() { + t.Fail() + panic(FailNow) +} + +func (t *T) Failed() bool { + return t.State == StateFailed +} + +func (t *T) Fatal(args ...interface{}) { + log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) + t.FailNow() +} + +func (t *T) Fatalf(format string, args ...interface{}) { + log.Ctx(t.ctx).Error().Msgf(format, args...) + t.FailNow() +} + +func (t *T) Log(args ...interface{}) { + log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) +} + +func (t *T) Logf(format string, args ...interface{}) { + log.Ctx(t.ctx).Info().Msgf(format, args...) +} + +func (t *T) Name() string { + return t.name +} + +func (t *T) Skip(args ...interface{}) { + log.Ctx(t.ctx).Info().Msg("Skip...") + log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) + if t.State == StateRunning { + t.State = StateSkipped + } +} + +func (t *T) SkipNow() { + log.Ctx(t.ctx).Info().Msg("Skip...") + if t.State == StateRunning { + t.State = StateSkipped + } + panic(SkipNow) +} + +func (t *T) Skipf(format string, args ...interface{}) { + log.Ctx(t.ctx).Info().Msg("Skip...") + log.Ctx(t.ctx).Info().Msgf(format, args...) + if t.State == StateRunning { + t.State = StateSkipped + } +} + +func (t *T) Skipped() bool { + return t.State == StateSkipped +} + +func (t *T) okIfNoSkipFail() { + if t.State == StateRunning { + t.State = StateOK + } +} diff --git a/tools/testserver/main.go b/tools/testserver/main.go index 53db6ed4b..76ea7e05c 100644 --- a/tools/testserver/main.go +++ b/tools/testserver/main.go @@ -19,6 +19,7 @@ import ( "lab.jamit.de/pace/go-microservice/maintenance/errors" "lab.jamit.de/pace/go-microservice/maintenance/log" _ "lab.jamit.de/pace/go-microservice/maintenance/tracing" + "lab.jamit.de/pace/go-microservice/testing/livetest" ) // pace lat/lon @@ -99,6 +100,23 @@ func main() { s := pacehttp.Server(h) log.Logger().Info().Str("addr", s.Addr).Msg("Starting testserver ...") + + go livetest.Test(context.Background(), []livetest.TestFunc{ + func(t *livetest.T) { + t.Log("Test /test query") + + resp, err := http.Get("http://localhost:3000/test") + if err != nil { + t.Error(err) + t.Fail() + } + if resp.StatusCode != 200 { + t.Log("Received status code: %d", resp.StatusCode) + t.Fail() + } + }, + }) + log.Fatal(s.ListenAndServe()) } From c1aa155bc81caedb2c5977edd9bb28d909c3c97c Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Thu, 7 Feb 2019 15:55:32 +0100 Subject: [PATCH 2/7] use log.Stack() #47 --- testing/livetest/livetest.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/testing/livetest/livetest.go b/testing/livetest/livetest.go index 5b5cdf36b..e89ebe9c0 100644 --- a/testing/livetest/livetest.go +++ b/testing/livetest/livetest.go @@ -6,8 +6,6 @@ package livetest import ( "context" "fmt" - "runtime/debug" - "strings" "time" opentracing "github.com/opentracing/opentracing-go" @@ -71,10 +69,7 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { defer func() { err := recover() if err != SkipNow || err != FailNow { - lines := strings.Split(string(debug.Stack()[:]), "\n") - for _, line := range lines { - proxy.Error(line) - } + log.Stack(ctx) proxy.Fail() } }() From 6e336fc7f0ecb2279c27c58b73dea404991e138e Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Fri, 8 Feb 2019 08:29:22 +0100 Subject: [PATCH 3/7] fix linting issues #47 --- client/nominatim/client.go | 4 +- client/pace/client/client.go | 6 +- client/pace/client/unixtime.go | 2 +- http/jsonapi/generator/generate_handler.go | 2 +- http/jsonapi/generator/generate_helper.go | 2 +- http/jsonapi/generator/generate_types.go | 2 +- .../generator/internal/poi/poi_test.go | 4 +- http/jsonapi/runtime/marshalling.go | 8 +-- http/jsonapi/runtime/parameters.go | 2 +- http/jsonapi/runtime/validation.go | 4 +- http/oauth2/introspection.go | 6 +- pkg/synctx/work_queue.go | 2 +- testing/livetest/livetest.go | 16 ++--- testing/livetest/test_proxy.go | 68 +++++++++++++------ tools/testserver/main.go | 2 +- 15 files changed, 78 insertions(+), 52 deletions(-) diff --git a/client/nominatim/client.go b/client/nominatim/client.go index c733b945b..950ced227 100644 --- a/client/nominatim/client.go +++ b/client/nominatim/client.go @@ -26,7 +26,7 @@ type Client struct { } // ErrUnableToGeocode lat/lon don't match to a known address -var ErrUnableToGeocode = errors.New("Unable to geocode") +var ErrUnableToGeocode = errors.New("unable to geocode") // ErrRequestFailed either the connection was lost or similar var ErrRequestFailed = errors.New("HTTP request failed") @@ -125,7 +125,7 @@ func (c *Client) Reverse(ctx context.Context, lat, lon float64, zoom int) (*Resu // prepate request u, err := url.Parse(c.Endpoint) if err != nil { - return nil, fmt.Errorf("Failed to parse nominatim endpoint URL %q: %v", c.Endpoint, err) + return nil, fmt.Errorf("failed to parse nominatim endpoint URL %q: %v", c.Endpoint, err) } u.Path = "nominatim/reverse" values := make(url.Values) diff --git a/client/pace/client/client.go b/client/pace/client/client.go index ba291eb75..71e5ab22f 100644 --- a/client/pace/client/client.go +++ b/client/pace/client/client.go @@ -21,7 +21,7 @@ import ( ) // ErrNotFound error for HTTP 404 responses -var ErrNotFound = errors.New("Resource not found (HTTP 404)") +var ErrNotFound = errors.New("resource not found (HTTP 404)") // ErrRequest contains error details type ErrRequest struct { @@ -61,7 +61,7 @@ func New(endpoint string) *Client { func (c *Client) URL(path string, values url.Values) (*url.URL, error) { u, err := url.Parse(c.Endpoint) if err != nil { - return nil, fmt.Errorf("Endpoint URL %q can't be parsed: %v", c.Endpoint, err) + return nil, fmt.Errorf("endpoint URL %q can't be parsed: %v", c.Endpoint, err) } u.Path = path @@ -156,7 +156,7 @@ func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, err // handle error if request failed if err != nil { - return nil, fmt.Errorf("Failed to %s %q: %v", req.Method, req.URL.String(), err) + return nil, fmt.Errorf("failed to %s %q: %v", req.Method, req.URL.String(), err) } // return not found diff --git a/client/pace/client/unixtime.go b/client/pace/client/unixtime.go index c3529f64c..47c4cda90 100644 --- a/client/pace/client/unixtime.go +++ b/client/pace/client/unixtime.go @@ -22,7 +22,7 @@ func (r UnixTime) MarshalJSON() ([]byte, error) { func (r *UnixTime) UnmarshalJSON(data []byte) error { var timestamp int64 if err := json.Unmarshal(data, ×tamp); err != nil { - return fmt.Errorf("Unix timestamp should be an number, got %s", data) + return fmt.Errorf("unix timestamp should be an number, got %s", data) } *r = UnixTime(time.Unix(timestamp, 0)) return nil diff --git a/http/jsonapi/generator/generate_handler.go b/http/jsonapi/generator/generate_handler.go index a5b252d0b..9dadbc8e0 100644 --- a/http/jsonapi/generator/generate_handler.go +++ b/http/jsonapi/generator/generate_handler.go @@ -159,7 +159,7 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.Swa // error responses have an error message parameter codeNum, err := strconv.Atoi(code) if err != nil { - return fmt.Errorf("Failed to parse response code %s: %v", code, err) + return fmt.Errorf("failed to parse response code %s: %v", code, err) } // generate method name diff --git a/http/jsonapi/generator/generate_helper.go b/http/jsonapi/generator/generate_helper.go index 55a5bf304..c6b31e782 100644 --- a/http/jsonapi/generator/generate_helper.go +++ b/http/jsonapi/generator/generate_helper.go @@ -69,7 +69,7 @@ func (g *Generator) goType(stmt *jen.Statement, schema *openapi3.Schema, tags ma return err } default: - return fmt.Errorf("Unknown type: %s", schema.Type) + return fmt.Errorf("unknown type: %s", schema.Type) } // add enum validation diff --git a/http/jsonapi/generator/generate_types.go b/http/jsonapi/generator/generate_types.go index b9d1771cc..31e6a368c 100644 --- a/http/jsonapi/generator/generate_types.go +++ b/http/jsonapi/generator/generate_types.go @@ -308,7 +308,7 @@ func (g *Generator) generateStructRelationships(prefix string, schema *openapi3. // check for data data := relSchema.Value.Properties["data"] if data == nil || data.Value == nil { - return nil, fmt.Errorf("No data for relationship %s context %s", relName, prefix) + return nil, fmt.Errorf("no data for relationship %s context %s", relName, prefix) } // generate relationship field diff --git a/http/jsonapi/generator/internal/poi/poi_test.go b/http/jsonapi/generator/internal/poi/poi_test.go index 625111ec9..98570f8c8 100644 --- a/http/jsonapi/generator/internal/poi/poi_test.go +++ b/http/jsonapi/generator/internal/poi/poi_test.go @@ -144,13 +144,15 @@ func TestHandler(t *testing.T) { err := json.NewDecoder(resp.Body).Decode(&data) if err != nil { t.Fatal(err) + return } if len(data.Data) != 10 { t.Error("Expected 10 apps") + return } - fmt.Printf("%#v", data) if data.Data[0]["type"] != "locationBasedApp" { t.Error("Expected type locationBasedApp") + return } meta, ok := data.Data[0]["meta"].(map[string]interface{}) if !ok { diff --git a/http/jsonapi/runtime/marshalling.go b/http/jsonapi/runtime/marshalling.go index ea8ebc0d1..6cbb11a09 100644 --- a/http/jsonapi/runtime/marshalling.go +++ b/http/jsonapi/runtime/marshalling.go @@ -25,7 +25,7 @@ func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { accept := r.Header.Get("Accept") if accept != JSONAPIContentType { WriteError(w, http.StatusNotAcceptable, - fmt.Errorf("Request needs to be send with %q header, containing value: %q", "Accept", JSONAPIContentType)) + fmt.Errorf("request needs to be send with %q header, containing value: %q", "Accept", JSONAPIContentType)) return false } @@ -33,7 +33,7 @@ func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { contentType := r.Header.Get("Content-Type") if contentType != JSONAPIContentType { WriteError(w, http.StatusUnsupportedMediaType, - fmt.Errorf("Request needs to be send with %q header, containing value: %q", "Accept", JSONAPIContentType)) + fmt.Errorf("request needs to be send with %q header, containing value: %q", "Accept", JSONAPIContentType)) return false } @@ -41,7 +41,7 @@ func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { err := jsonapi.UnmarshalPayload(r.Body, data) if err != nil { WriteError(w, http.StatusUnprocessableEntity, - fmt.Errorf("Can't parse content: %v", err)) + fmt.Errorf("can't parse content: %v", err)) return false } @@ -59,6 +59,6 @@ func Marshal(w http.ResponseWriter, data interface{}, code int) { // write marshaled response body err := jsonapi.MarshalPayload(w, data) if err != nil { - panic(fmt.Errorf("Failed to marshal jsonapi response for %#v: %s", data, err)) + panic(fmt.Errorf("failed to marshal jsonapi response for %#v: %s", data, err)) } } diff --git a/http/jsonapi/runtime/parameters.go b/http/jsonapi/runtime/parameters.go index 480587e1a..3ae8f63ca 100644 --- a/http/jsonapi/runtime/parameters.go +++ b/http/jsonapi/runtime/parameters.go @@ -87,7 +87,7 @@ func ScanParameters(w http.ResponseWriter, r *http.Request, parameters ...*ScanP // single parameter scanning scanData = strings.Join(input, " ") default: - panic(fmt.Errorf("Impossible scanning location: %d", param.Location)) + panic(fmt.Errorf("impossible scanning location: %d", param.Location)) } n, _ := fmt.Sscan(scanData, param.Data) // nolint: gosec diff --git a/http/jsonapi/runtime/validation.go b/http/jsonapi/runtime/validation.go index 834d298f9..34d59b6a1 100644 --- a/http/jsonapi/runtime/validation.go +++ b/http/jsonapi/runtime/validation.go @@ -41,7 +41,7 @@ func ValidateStruct(w http.ResponseWriter, r *http.Request, data interface{}, so case error: panic(err) // programming error, e.g. not used with struct default: - panic(fmt.Errorf("Unhandled error case: %s", err)) + panic(fmt.Errorf("unhandled error case: %s", err)) } return false @@ -59,7 +59,7 @@ func generateValidationErrors(validErrors valid.Errors, jsonapiErrors *Errors, s case valid.Error: *jsonapiErrors = append(*jsonapiErrors, generateValidationError(e, source)) default: - panic(fmt.Errorf("Unhandled error case: %s", e)) + panic(fmt.Errorf("unhandled error case: %s", e)) } } } diff --git a/http/oauth2/introspection.go b/http/oauth2/introspection.go index 277f37cfe..ad9ac2e1d 100644 --- a/http/oauth2/introspection.go +++ b/http/oauth2/introspection.go @@ -14,9 +14,9 @@ import ( type introspecter func(mdw *Middleware, token string, resp *introspectResponse) error -var errInvalidToken = errors.New("User token is invalid") -var errUpstreamConnection = errors.New("Problem connecting to the introspection endpoint") -var errBadUpstreamResponse = errors.New("Bad upstream response when introspecting token") +var errInvalidToken = errors.New("user token is invalid") +var errUpstreamConnection = errors.New("problem connecting to the introspection endpoint") +var errBadUpstreamResponse = errors.New("bad upstream response when introspecting token") type introspectResponse struct { Active bool `json:"active"` diff --git a/pkg/synctx/work_queue.go b/pkg/synctx/work_queue.go index af9418b29..d9bfde2fd 100644 --- a/pkg/synctx/work_queue.go +++ b/pkg/synctx/work_queue.go @@ -45,7 +45,7 @@ func (queue *WorkQueue) Add(description string, fn WorkFunc) { // if one of the work queue items fails the whole // queue will be canceled if err != nil { - queue.setErr(fmt.Errorf("Failed to %s: %v", description, err)) + queue.setErr(fmt.Errorf("failed to %s: %v", description, err)) queue.cancel() } queue.wg.Done() diff --git a/testing/livetest/livetest.go b/testing/livetest/livetest.go index e89ebe9c0..d17be9890 100644 --- a/testing/livetest/livetest.go +++ b/testing/livetest/livetest.go @@ -46,14 +46,14 @@ func testRun(ctx context.Context, tests []TestFunc) { Int("test", i+1).Logger() ctx = logger.WithContext(ctx) - err := executeTest(ctx, test, fmt.Sprintf("test-%d", i+1)) + err = executeTest(ctx, test, fmt.Sprintf("test-%d", i+1)) if err != nil { break } } if err != nil { - log.Errorf("Failed to run tests: %v", err) + log.Errorf("failed to run tests: %v", err) } } @@ -68,7 +68,7 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { func() { defer func() { err := recover() - if err != SkipNow || err != FailNow { + if err != ErrSkipNow || err != ErrFailNow { log.Stack(ctx) proxy.Fail() } @@ -80,21 +80,21 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { proxy.okIfNoSkipFail() paceLivetestDurationSeconds.WithLabelValues(cfg.ServiceName).Observe(duration) - switch proxy.State { + switch proxy.state { case StateFailed: logger.Warn().Msg("Test failed!") - span.LogEvent("Test failed!") + span.LogKV("test", "failed") paceLivetestTotal.WithLabelValues(cfg.ServiceName, "failed").Add(1) case StateOK: logger.Info().Msg("Test succeeded!") - span.LogEvent("Test succeeded!") + span.LogKV("test", "succeeded") paceLivetestTotal.WithLabelValues(cfg.ServiceName, "succeeded").Add(1) case StateSkipped: logger.Info().Msg("Test skipped!") - span.LogEvent("Test skipped!") + span.LogKV("test", "skipped") paceLivetestTotal.WithLabelValues(cfg.ServiceName, "skipped").Add(1) default: - panic(fmt.Errorf("Invalid state: %v", proxy.State)) + panic(fmt.Errorf("invalid state: %v", proxy.state)) } return nil diff --git a/testing/livetest/test_proxy.go b/testing/livetest/test_proxy.go index 2b93a47e7..98d3e4dac 100644 --- a/testing/livetest/test_proxy.go +++ b/testing/livetest/test_proxy.go @@ -11,107 +11,131 @@ import ( "lab.jamit.de/pace/go-microservice/maintenance/log" ) -// SkipNow is used as a panic if SkipNow is called on the test -var SkipNow = errors.New("skipped test") +// ErrSkipNow is used as a panic if ErrSkipNow is called on the test +var ErrSkipNow = errors.New("skipped test") -// FailNow is used as a panic if FailNow is called on the test -var FailNow = errors.New("failed test") +// ErrFailNow is used as a panic if ErrFailNow is called on the test +var ErrFailNow = errors.New("failed test") +// TestState represents the state of a test type TestState string var ( + // StateRunning first state StateRunning TestState = "running" - StateOK TestState = "ok" - StateFailed TestState = "failed" + // StateOK test was executed without failure + StateOK TestState = "ok" + // StateFailed test was executed with failure + StateFailed TestState = "failed" + // StateSkipped test was skipped StateSkipped TestState = "skipped" ) +// T implements a similar interface than testing.T type T struct { name string ctx context.Context - State TestState + state TestState } +// NewTestProxy creates a new text proxy using the given context +// and name. func NewTestProxy(ctx context.Context, name string) *T { - return &T{name: name, ctx: ctx, State: StateRunning} + return &T{name: name, ctx: ctx, state: StateRunning} } +// Error logs an error message with the test func (t *T) Error(args ...interface{}) { log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) + t.Fail() } +// Errorf logs an error message with the test func (t *T) Errorf(format string, args ...interface{}) { log.Ctx(t.ctx).Error().Msgf(format, args...) + t.Fail() } +// Fail marks the test as failed func (t *T) Fail() { log.Ctx(t.ctx).Info().Msg("Fail...") - if t.State == StateRunning { - t.State = StateFailed + if t.state == StateRunning { + t.state = StateFailed } } +// FailNow marks the test as failed and skips further execution func (t *T) FailNow() { t.Fail() - panic(FailNow) + panic(ErrFailNow) } +// Failed returns true if the test was marked as failed func (t *T) Failed() bool { - return t.State == StateFailed + return t.state == StateFailed } +// Fatal logs the passed message in the context of the test and fails the test func (t *T) Fatal(args ...interface{}) { log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) t.FailNow() } +// Fatalf logs the passed message in the context of the test and fails the test func (t *T) Fatalf(format string, args ...interface{}) { log.Ctx(t.ctx).Error().Msgf(format, args...) t.FailNow() } +// Log logs the passed message in the context of the test func (t *T) Log(args ...interface{}) { log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) } +// Logf logs the passed message in the context of the test func (t *T) Logf(format string, args ...interface{}) { log.Ctx(t.ctx).Info().Msgf(format, args...) } +// Name returns the name of the test func (t *T) Name() string { return t.name } +// Skip logs reason and marks the test as skipped func (t *T) Skip(args ...interface{}) { log.Ctx(t.ctx).Info().Msg("Skip...") log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) - if t.State == StateRunning { - t.State = StateSkipped + if t.state == StateRunning { + t.state = StateSkipped } } +// SkipNow skips the test immediately func (t *T) SkipNow() { log.Ctx(t.ctx).Info().Msg("Skip...") - if t.State == StateRunning { - t.State = StateSkipped + if t.state == StateRunning { + t.state = StateSkipped } - panic(SkipNow) + panic(ErrSkipNow) } +// Skipf marks the test as skippend and log a reason func (t *T) Skipf(format string, args ...interface{}) { log.Ctx(t.ctx).Info().Msg("Skip...") log.Ctx(t.ctx).Info().Msgf(format, args...) - if t.State == StateRunning { - t.State = StateSkipped + if t.state == StateRunning { + t.state = StateSkipped } } +// Skipped returns true if the test was skipped func (t *T) Skipped() bool { - return t.State == StateSkipped + return t.state == StateSkipped } func (t *T) okIfNoSkipFail() { - if t.State == StateRunning { - t.State = StateOK + if t.state == StateRunning { + t.state = StateOK } } diff --git a/tools/testserver/main.go b/tools/testserver/main.go index 76ea7e05c..8dcf9a46c 100644 --- a/tools/testserver/main.go +++ b/tools/testserver/main.go @@ -111,7 +111,7 @@ func main() { t.Fail() } if resp.StatusCode != 200 { - t.Log("Received status code: %d", resp.StatusCode) + t.Logf("Received status code: %d", resp.StatusCode) t.Fail() } }, From eb464456f1d53e5f49ca9204c18d3de3c7b568d9 Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Fri, 8 Feb 2019 13:26:05 +0100 Subject: [PATCH 4/7] fix tests #47 --- client/nominatim/client.go | 12 ++++++------ client/nominatim/client_test.go | 6 ++++-- pkg/synctx/work_queue_test.go | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/nominatim/client.go b/client/nominatim/client.go index 950ced227..30de0856e 100644 --- a/client/nominatim/client.go +++ b/client/nominatim/client.go @@ -57,7 +57,7 @@ func (c *Client) SolidifiedReverse(ctx context.Context, lat, lon float64) (*Resu return nil, err } - if resultHigh != nil { + if resultHigh != nil && resultHigh.Address != nil && resultLow != nil && resultLow.Address != nil { if resultLow.Address.City == "" { resultLow.Address.City = resultHigh.Address.City } @@ -82,6 +82,8 @@ func (c *Client) SolidifiedReverse(ctx context.Context, lat, lon float64) (*Resu if resultLow.Address.State == "" { resultLow.Address.State = resultHigh.Address.State } + } else { + return nil, ErrUnableToGeocode } return resultLow, nil @@ -147,8 +149,7 @@ func (c *Client) Reverse(ctx context.Context, lat, lon float64, zoom int) (*Resu defer resp.Body.Close() // nolint: errcheck if resp.StatusCode != http.StatusOK { - err = ErrRequestFailed - return nil, err + return nil, ErrRequestFailed } // parse response @@ -158,9 +159,8 @@ func (c *Client) Reverse(ctx context.Context, lat, lon float64, zoom int) (*Resu } // Handle geocoding error - if result.Error == ErrUnableToGeocode.Error() { - err = ErrUnableToGeocode - return nil, err + if result.Error == "Unable to geocode" { + return nil, ErrUnableToGeocode } return &result, nil diff --git a/client/nominatim/client_test.go b/client/nominatim/client_test.go index c7804eaab..bbad02684 100644 --- a/client/nominatim/client_test.go +++ b/client/nominatim/client_test.go @@ -14,14 +14,16 @@ func TestIntegrationReverse(t *testing.T) { ctx := context.Background() ctx = log.With().Logger().WithContext(ctx) - _, err := DefaultClient.Reverse(ctx, 0, 0, 18) + d, err := DefaultClient.Reverse(ctx, 0, 0, 18) if err != ErrUnableToGeocode { - t.Error("expected unable to geocode error, got: ", err) + t.Errorf("expected unable to geocode error, got: %v; %+v", err, d) + return } res, err := DefaultClient.Reverse(ctx, 49.01251, 8.42636, 18) if err != nil { t.Error("expected error, got: ", err) + return } expected := "Haid-und-Neu-Straße 18, 76131 Karlsruhe" diff --git a/pkg/synctx/work_queue_test.go b/pkg/synctx/work_queue_test.go index f7ad559b2..f7a536a69 100644 --- a/pkg/synctx/work_queue_test.go +++ b/pkg/synctx/work_queue_test.go @@ -47,7 +47,7 @@ func TestWorkQueueOneTaskWithErr(t *testing.T) { t.Error("expected error") return } - expected := "Failed to some work: Some error" + expected := "failed to some work: Some error" if q.Err().Error() != expected { t.Errorf("expected error %q, got: %q", q.Err().Error(), expected) } From 0bbcea662857f21734b86dba43a3fd10c50e4a3a Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Mon, 11 Feb 2019 08:32:33 +0100 Subject: [PATCH 5/7] adds tests and fixes ok tests #47 --- testing/livetest/livetest.go | 2 +- testing/livetest/livetest_example_test.go | 57 +++++++++++++++++++++++ testing/livetest/livetest_test.go | 49 ++++++++++++------- 3 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 testing/livetest/livetest_example_test.go diff --git a/testing/livetest/livetest.go b/testing/livetest/livetest.go index d17be9890..30291f14e 100644 --- a/testing/livetest/livetest.go +++ b/testing/livetest/livetest.go @@ -68,7 +68,7 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { func() { defer func() { err := recover() - if err != ErrSkipNow || err != ErrFailNow { + if err != nil && (err != ErrSkipNow || err != ErrFailNow) { log.Stack(ctx) proxy.Fail() } diff --git a/testing/livetest/livetest_example_test.go b/testing/livetest/livetest_example_test.go new file mode 100644 index 000000000..8e95617f1 --- /dev/null +++ b/testing/livetest/livetest_example_test.go @@ -0,0 +1,57 @@ +// Copyright © 2018 by PACE Telematics GmbH. All rights reserved. +// Created at 2019/02/01 by Vincent Landgraf + +package livetest_test + +import ( + "context" + "log" + "time" + + "lab.jamit.de/pace/go-microservice/testing/livetest" +) + +func ExampleTest() { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) + defer cancel() + + err := livetest.Test(ctx, []livetest.TestFunc{ + func(t *livetest.T) { + t.Logf("Executed test no %d", 1) + }, + func(t *livetest.T) { + t.Log("Executed test no 2") + }, + func(t *livetest.T) { + t.Fatal("Fail test no 3") + }, + func(t *livetest.T) { + t.Fatalf("Fail test no %d", 4) + }, + func(t *livetest.T) { + t.Skip("Skipping test no 5") + }, + func(t *livetest.T) { + t.Skipf("Skipping test no %d", 5) + }, + func(t *livetest.T) { + t.SkipNow() + }, + func(t *livetest.T) { + t.Fail() + }, + func(t *livetest.T) { + t.FailNow() + }, + func(t *livetest.T) { + t.Error("Some") + }, + func(t *livetest.T) { + t.Errorf("formatted") + }, + }) + if err != context.DeadlineExceeded { + log.Fatal(err) + } + // Output: +} diff --git a/testing/livetest/livetest_test.go b/testing/livetest/livetest_test.go index 8e95617f1..5613776b6 100644 --- a/testing/livetest/livetest_test.go +++ b/testing/livetest/livetest_test.go @@ -1,57 +1,70 @@ // Copyright © 2018 by PACE Telematics GmbH. All rights reserved. // Created at 2019/02/01 by Vincent Landgraf -package livetest_test +package livetest import ( "context" - "log" + "net/http/httptest" + "strings" + "testing" "time" - "lab.jamit.de/pace/go-microservice/testing/livetest" + "lab.jamit.de/pace/go-microservice/maintenance/metrics" ) -func ExampleTest() { +func TestExample(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() - err := livetest.Test(ctx, []livetest.TestFunc{ - func(t *livetest.T) { + err := Test(ctx, []TestFunc{ + func(t *T) { t.Logf("Executed test no %d", 1) }, - func(t *livetest.T) { + func(t *T) { t.Log("Executed test no 2") }, - func(t *livetest.T) { + func(t *T) { t.Fatal("Fail test no 3") }, - func(t *livetest.T) { + func(t *T) { t.Fatalf("Fail test no %d", 4) }, - func(t *livetest.T) { + func(t *T) { t.Skip("Skipping test no 5") }, - func(t *livetest.T) { + func(t *T) { t.Skipf("Skipping test no %d", 5) }, - func(t *livetest.T) { + func(t *T) { t.SkipNow() }, - func(t *livetest.T) { + func(t *T) { t.Fail() }, - func(t *livetest.T) { + func(t *T) { t.FailNow() }, - func(t *livetest.T) { + func(t *T) { t.Error("Some") }, - func(t *livetest.T) { + func(t *T) { t.Errorf("formatted") }, }) if err != context.DeadlineExceeded { - log.Fatal(err) + t.Error(err) + return + } + + req := httptest.NewRequest("GET", "/metrics", nil) + resp := httptest.NewRecorder() + metrics.Handler().ServeHTTP(resp, req) + body := resp.Body.String() + + if !strings.Contains(body, `pace_livetest_total{result="failed"d,service="go-microservice"} 6`) || + !strings.Contains(body, `pace_livetest_total{result="skipped",service="go-microservice"} 3`) || + !strings.Contains(body, `pace_livetest_total{result="succeeded",service="go-microservice"} 2`) { + t.Error("expected other pace_livetest_total counts") } - // Output: } From a7be6230a479c185a967ffb986cf68f6aca4fd0b Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Mon, 11 Feb 2019 08:46:51 +0100 Subject: [PATCH 6/7] make test more resilient for the CI system #47 --- testing/livetest/livetest_test.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/testing/livetest/livetest_test.go b/testing/livetest/livetest_test.go index 5613776b6..b6167b4ae 100644 --- a/testing/livetest/livetest_test.go +++ b/testing/livetest/livetest_test.go @@ -13,7 +13,12 @@ import ( "lab.jamit.de/pace/go-microservice/maintenance/metrics" ) -func TestExample(t *testing.T) { +func TestIntegrationExample(t *testing.T) { + if testing.Short() { + t.Skip() + return + } + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() @@ -62,9 +67,14 @@ func TestExample(t *testing.T) { metrics.Handler().ServeHTTP(resp, req) body := resp.Body.String() - if !strings.Contains(body, `pace_livetest_total{result="failed"d,service="go-microservice"} 6`) || - !strings.Contains(body, `pace_livetest_total{result="skipped",service="go-microservice"} 3`) || - !strings.Contains(body, `pace_livetest_total{result="succeeded",service="go-microservice"} 2`) { - t.Error("expected other pace_livetest_total counts") + for i := 0; i < 10; i++ { + if strings.Contains(body, `pace_livetest_total{result="failed",service="go-microservice"} 6`) && + strings.Contains(body, `pace_livetest_total{result="skipped",service="go-microservice"} 3`) && + strings.Contains(body, `pace_livetest_total{result="succeeded",service="go-microservice"} 2`) { + return // test os ok + } + time.Sleep(time.Millisecond * 10) } + + t.Errorf("expected other pace_livetest_total counts, got: %v", body) } From aacd32852de17bdce13f9ac06dbfec894c722431 Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Mon, 11 Feb 2019 09:02:31 +0100 Subject: [PATCH 7/7] adapt to the service name used in tests #47 --- testing/livetest/livetest_test.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/testing/livetest/livetest_test.go b/testing/livetest/livetest_test.go index b6167b4ae..38508c0b4 100644 --- a/testing/livetest/livetest_test.go +++ b/testing/livetest/livetest_test.go @@ -67,14 +67,10 @@ func TestIntegrationExample(t *testing.T) { metrics.Handler().ServeHTTP(resp, req) body := resp.Body.String() - for i := 0; i < 10; i++ { - if strings.Contains(body, `pace_livetest_total{result="failed",service="go-microservice"} 6`) && - strings.Contains(body, `pace_livetest_total{result="skipped",service="go-microservice"} 3`) && - strings.Contains(body, `pace_livetest_total{result="succeeded",service="go-microservice"} 2`) { - return // test os ok - } - time.Sleep(time.Millisecond * 10) + sn := cfg.ServiceName + if !strings.Contains(body, `pace_livetest_total{result="failed",service="`+sn+`"} 6`) || + !strings.Contains(body, `pace_livetest_total{result="skipped",service="`+sn+`"} 3`) || + !strings.Contains(body, `pace_livetest_total{result="succeeded",service="`+sn+`"} 2`) { + t.Errorf("expected other pace_livetest_total counts, got: %v", body) } - - t.Errorf("expected other pace_livetest_total counts, got: %v", body) }