add log, slugify, herror and test packages
Adds log, slugify, herror and test packages initially created for Hungry Hippo project. This also includes a Makefile to help testing all packages, consolidate coverage report, exame Go source and ensure format, a gitlab-cy.yml file. HH-86
This commit is contained in:
parent
1b94ad4658
commit
945d4caf81
|
@ -0,0 +1,4 @@
|
||||||
|
.DS_Store
|
||||||
|
debug.*
|
||||||
|
coverage/coverage.*
|
||||||
|
coverage/*cover.out
|
|
@ -0,0 +1,13 @@
|
||||||
|
image: golang:1.12
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
|
||||||
|
variables:
|
||||||
|
GO111MODULE: "on"
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- cd ${CI_PROJECT_DIR}
|
||||||
|
- make
|
|
@ -0,0 +1,50 @@
|
||||||
|
# this Makefile purpose is to help testing all packages, consolidate coverage
|
||||||
|
# report, exame Go source and ensure format.
|
||||||
|
SRC = $(shell find . -type f -name '*.go' | \
|
||||||
|
awk -F'__' '{ sub ("/[^/]*$$", "/", $$1); print $1 }' | sort | uniq)
|
||||||
|
|
||||||
|
PACKAGES = log slugify herror test
|
||||||
|
|
||||||
|
all: dep fmt vet test_all
|
||||||
|
|
||||||
|
dep:
|
||||||
|
@for pkg in $(PACKAGES); do \
|
||||||
|
echo "- Checking dependencies for $$pkg"; \
|
||||||
|
cd $$pkg && go get && cd ..; \
|
||||||
|
done
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@echo "- Checking code format"
|
||||||
|
@GO_FMT=$$(gofmt -e -l ${SRC}) && \
|
||||||
|
if [ -n "$$GO_FMT" ]; then \
|
||||||
|
echo '$@: Incorrect format has been detected in your code run `make fmt-fix`'; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
fmt-fix:
|
||||||
|
@echo "- Checking code format"
|
||||||
|
@for file in $$(go fmt ${SRC}) ; do \
|
||||||
|
echo "$@: $$file fixed and staged"; \
|
||||||
|
git add "./${file}"; \
|
||||||
|
done
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@for pkg in $(PACKAGES); do \
|
||||||
|
echo "- Examine source code for $$pkg"; \
|
||||||
|
cd $$pkg && go vet . && cd ..; \
|
||||||
|
done
|
||||||
|
|
||||||
|
test_all:
|
||||||
|
@rm -f ./coverage/*.out ./coverage/*.html
|
||||||
|
@for pkg in $(PACKAGES); do \
|
||||||
|
echo "- Testing package $$pkg"; \
|
||||||
|
go test ./$$pkg -coverprofile=./coverage/$$pkg.cover.out; \
|
||||||
|
done
|
||||||
|
@echo "- Merging coverage output files"
|
||||||
|
@echo "mode: set" > ./coverage/coverage.out && \
|
||||||
|
cat ./coverage/*.cover.out | grep -v mode: | sort -r | \
|
||||||
|
awk -f ./coverage/merge.awk >> ./coverage/coverage.out
|
||||||
|
@go tool cover -html=./coverage/coverage.out \
|
||||||
|
-o ./coverage/coverage.html
|
||||||
|
@go tool cover --func=./coverage/coverage.out | \
|
||||||
|
awk -f ./coverage/total_coverage.awk
|
|
@ -1,3 +1,3 @@
|
||||||
# KIT
|
# KIT [![pipeline status](https://dev-git.sentia.com/go/kit/badges/master/pipeline.svg)](https://dev-git.sentia.com/go/kit/commits/master) [![coverage report](https://dev-git.sentia.com/go/kit/badges/master/coverage.svg)](https://dev-git.sentia.com/go/kit/commits/master)
|
||||||
|
|
||||||
Bundle with most used packages for development.
|
Bundle with most used packages for development.
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
if (last != $1)
|
||||||
|
print $0
|
||||||
|
last = $1
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
if ($0 ~ /^total\:/)
|
||||||
|
print "coverage: " $3 " of statements";
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
module dev-git.sentia.com/go/kit
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/go-kit/kit v0.9.0
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0 // indirect
|
||||||
|
golang.org/x/text v0.3.2
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||||
|
)
|
|
@ -0,0 +1,12 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
@ -0,0 +1,29 @@
|
||||||
|
package herror_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/herror"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrTaskFailed = herror.New("task has failed")
|
||||||
|
|
||||||
|
func step() error {
|
||||||
|
return fmt.Errorf("cannot move")
|
||||||
|
}
|
||||||
|
|
||||||
|
func performTask() error {
|
||||||
|
if err := step(); err != nil {
|
||||||
|
return ErrTaskFailed.Wrap(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
if err := performTask(); err != nil {
|
||||||
|
fmt.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Output: task has failed
|
||||||
|
//-> cannot move
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package herror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Err represents an error
|
||||||
|
type Err struct {
|
||||||
|
error string
|
||||||
|
wrapped *Err
|
||||||
|
details string
|
||||||
|
stack *Stacktrace
|
||||||
|
}
|
||||||
|
|
||||||
|
type errJSON struct {
|
||||||
|
E string `json:"error"`
|
||||||
|
W *Err `json:"wrapped"`
|
||||||
|
D string `json:"details"`
|
||||||
|
S *Stacktrace `json:"stack"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new instance for Err type with assigned error
|
||||||
|
func New(err interface{}) *Err {
|
||||||
|
newerror := new(Err)
|
||||||
|
|
||||||
|
switch e := err.(type) {
|
||||||
|
case string:
|
||||||
|
newerror.error = e
|
||||||
|
|
||||||
|
case error:
|
||||||
|
if castErr, ok := e.(*Err); ok {
|
||||||
|
return castErr
|
||||||
|
}
|
||||||
|
newerror.error = e.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newerror
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap set an error that is wrapped by Err
|
||||||
|
func Wrap(err, errwrapped error) error {
|
||||||
|
newerr := New(err)
|
||||||
|
return newerr.Wrap(errwrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns a wrapped error if present
|
||||||
|
func Unwrap(err error) error {
|
||||||
|
return xerrors.Unwrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is reports whether any error in err's chain matches target.
|
||||||
|
func Is(err, target error) bool {
|
||||||
|
return xerrors.Is(err, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap set an error that is wrapped by Err
|
||||||
|
func (e *Err) Wrap(err error) *Err {
|
||||||
|
wrapped := New(err)
|
||||||
|
|
||||||
|
if deeper := xerrors.Unwrap(err); deeper != nil {
|
||||||
|
Wrap(wrapped, deeper)
|
||||||
|
}
|
||||||
|
|
||||||
|
newerr := &Err{
|
||||||
|
error: e.error,
|
||||||
|
wrapped: e.wrapped,
|
||||||
|
details: e.details,
|
||||||
|
stack: e.stack,
|
||||||
|
}
|
||||||
|
newerr.wrapped = wrapped
|
||||||
|
return newerr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns wrapped error
|
||||||
|
func (e *Err) Unwrap() error {
|
||||||
|
if e.wrapped == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is reports whether an error matches.
|
||||||
|
func (e *Err) Is(err error) bool {
|
||||||
|
if e.wrapped != nil {
|
||||||
|
return e.error == err.Error() || e.wrapped.Is(err)
|
||||||
|
}
|
||||||
|
return e.error == err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureStack sets stack traces when the method is called
|
||||||
|
func (e *Err) CaptureStack() *Err {
|
||||||
|
e.stack = NewStacktrace()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack returns full stack traces
|
||||||
|
func (e *Err) Stack() *Stacktrace {
|
||||||
|
return e.stack
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDetails records variable info to the error mostly for debugging purposes
|
||||||
|
func (e *Err) AddDetails(v ...interface{}) *Err {
|
||||||
|
buff := new(bytes.Buffer)
|
||||||
|
fmt.Fprintln(buff, e.details)
|
||||||
|
spew.Fdump(buff, v...)
|
||||||
|
e.details = buff.String()
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details returns error's details
|
||||||
|
func (e *Err) Details() string {
|
||||||
|
return e.details
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors return a composed message of the assigned error e wrapped error
|
||||||
|
func (e *Err) Error() string {
|
||||||
|
if e.wrapped == nil {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s\n-> %s", e.error, e.wrapped.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON
|
||||||
|
func (e *Err) UnmarshalJSON(b []byte) error {
|
||||||
|
var errJSON errJSON
|
||||||
|
if err := json.Unmarshal(b, &errJSON); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = Err{
|
||||||
|
error: errJSON.E,
|
||||||
|
wrapped: errJSON.W,
|
||||||
|
details: errJSON.D,
|
||||||
|
stack: errJSON.S,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON
|
||||||
|
func (e *Err) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(errJSON{
|
||||||
|
E: e.error,
|
||||||
|
W: e.wrapped,
|
||||||
|
D: e.details,
|
||||||
|
S: e.stack,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
package herror_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/herror"
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHError(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("new error", func(t *testing.T) {
|
||||||
|
errDefault := "this is an error"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
input interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "string",
|
||||||
|
input: errDefault,
|
||||||
|
expected: errDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "error",
|
||||||
|
input: fmt.Errorf(errDefault),
|
||||||
|
expected: errDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "herror.Err",
|
||||||
|
input: herror.New(errDefault),
|
||||||
|
expected: errDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "invalid type",
|
||||||
|
input: 123456789,
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
test.Equals(t, tc.expected, herror.New(tc.input).Error())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrap", func(t *testing.T) {
|
||||||
|
errmain := herror.New("MAIN ERROR")
|
||||||
|
errfmt := fmt.Errorf("ERROR FORMATTED")
|
||||||
|
errA := herror.New("ERR A")
|
||||||
|
errB := herror.New("ERR B")
|
||||||
|
errC := herror.New("ERR C")
|
||||||
|
errD := herror.New("ERR D")
|
||||||
|
errNested := errmain.Wrap(
|
||||||
|
errA.Wrap(
|
||||||
|
errB.Wrap(
|
||||||
|
errC.Wrap(errD),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
err error
|
||||||
|
expected []error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "error",
|
||||||
|
err: errfmt,
|
||||||
|
expected: []error{
|
||||||
|
errfmt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "deeper nested wrap",
|
||||||
|
err: errNested,
|
||||||
|
expected: []error{
|
||||||
|
errA, errB, errC, errD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
newerr := errmain.Wrap(tc.err)
|
||||||
|
|
||||||
|
for _, e := range tc.expected {
|
||||||
|
test.Equals(t, true, newerr.Is(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json marshalling", func(t *testing.T) {
|
||||||
|
hError := herror.New("this is an error").
|
||||||
|
Wrap(fmt.Errorf("this is another error")).
|
||||||
|
CaptureStack()
|
||||||
|
marshalled, err := json.Marshal(hError)
|
||||||
|
test.OK(t, err)
|
||||||
|
|
||||||
|
var unmarshalled *herror.Err
|
||||||
|
test.OK(t, json.Unmarshal(marshalled, &unmarshalled))
|
||||||
|
test.Equals(t, hError, unmarshalled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErr_Wrap() {
|
||||||
|
errA := herror.New("something went wrong")
|
||||||
|
errB := fmt.Errorf("because of this error")
|
||||||
|
newerr := herror.Wrap(errA, errB)
|
||||||
|
|
||||||
|
fmt.Print(herror.Unwrap(newerr), "\n", newerr)
|
||||||
|
// Output: because of this error
|
||||||
|
// something went wrong
|
||||||
|
// -> because of this error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErr_Is() {
|
||||||
|
errA := herror.New("something went wrong")
|
||||||
|
errB := func() error {
|
||||||
|
return errA
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Print(herror.Is(errA, errB))
|
||||||
|
// Output: true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErr_CaptureStack() {
|
||||||
|
err := herror.New("something went wrong")
|
||||||
|
err.CaptureStack()
|
||||||
|
|
||||||
|
fmt.Print(err, "\n", err.Stack().Frames[2].Function)
|
||||||
|
// Output: something went wrong
|
||||||
|
// ExampleErr_CaptureStack
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErr_AddDetails() {
|
||||||
|
err := herror.New("something went wrong")
|
||||||
|
err.AddDetails(struct {
|
||||||
|
number int
|
||||||
|
}{123})
|
||||||
|
|
||||||
|
fmt.Print(err, err.Details())
|
||||||
|
// Output: something went wrong
|
||||||
|
// (struct { number int }) {
|
||||||
|
// number: (int) 123
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package herror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stacktrace holds information about the frames of the stack.
|
||||||
|
type Stacktrace struct {
|
||||||
|
Frames []Frame `json:"frames,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame represents parsed information from runtime.Frame
|
||||||
|
type Frame struct {
|
||||||
|
Function string `json:"function,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Package string `json:"package,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
AbsPath string `json:"abs_path,omitempty"`
|
||||||
|
Line int `json:"line,omitempty"`
|
||||||
|
InApp bool `json:"in_app,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FrameFilter represents function to filter frames
|
||||||
|
type FrameFilter func(Frame) bool
|
||||||
|
|
||||||
|
const unknown string = "unknown"
|
||||||
|
|
||||||
|
// NewStacktrace creates a stacktrace using `runtime.Callers`.
|
||||||
|
func NewStacktrace(filters ...FrameFilter) *Stacktrace {
|
||||||
|
pcs := make([]uintptr, 100)
|
||||||
|
n := runtime.Callers(1, pcs)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
frames := extractFrames(pcs[:n])
|
||||||
|
|
||||||
|
// default filter
|
||||||
|
frames = filterFrames(frames, func(f Frame) bool {
|
||||||
|
return f.Package == "runtime" || f.Package == "testing" ||
|
||||||
|
strings.HasSuffix(f.Package, "/herror")
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
frames = filterFrames(frames, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
stacktrace := Stacktrace{
|
||||||
|
Frames: frames,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stacktrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFrame assembles a stacktrace frame out of `runtime.Frame`.
|
||||||
|
func NewFrame(f runtime.Frame) Frame {
|
||||||
|
abspath := unknown
|
||||||
|
filename := unknown
|
||||||
|
if f.File != "" {
|
||||||
|
abspath = f.File
|
||||||
|
_, filename = filepath.Split(f.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
function := unknown
|
||||||
|
pkgname := unknown
|
||||||
|
typer := ""
|
||||||
|
if f.Function != "" {
|
||||||
|
pkgname, typer, function = deconstructFunctionName(f.Function)
|
||||||
|
}
|
||||||
|
|
||||||
|
inApp := func() bool {
|
||||||
|
out := strings.HasPrefix(abspath, build.Default.GOROOT) ||
|
||||||
|
strings.Contains(pkgname, "vendor")
|
||||||
|
return !out
|
||||||
|
}()
|
||||||
|
|
||||||
|
return Frame{
|
||||||
|
AbsPath: abspath,
|
||||||
|
Filename: filename,
|
||||||
|
Line: f.Line,
|
||||||
|
Package: pkgname,
|
||||||
|
Type: typer,
|
||||||
|
Function: function,
|
||||||
|
InApp: inApp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterFrames(frames []Frame, filter FrameFilter) []Frame {
|
||||||
|
filtered := make([]Frame, 0, len(frames))
|
||||||
|
|
||||||
|
for _, frame := range frames {
|
||||||
|
if filter(frame) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFrames(pcs []uintptr) []Frame {
|
||||||
|
frames := make([]Frame, 0, len(pcs))
|
||||||
|
callersFrames := runtime.CallersFrames(pcs)
|
||||||
|
|
||||||
|
for {
|
||||||
|
callerFrame, more := callersFrames.Next()
|
||||||
|
frames = append([]Frame{
|
||||||
|
NewFrame(callerFrame),
|
||||||
|
}, frames...)
|
||||||
|
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func deconstructFunctionName(name string) (pkg string, typer string, function string) {
|
||||||
|
if i := strings.LastIndex(name, "/"); i != -1 {
|
||||||
|
pkg = name[:i]
|
||||||
|
function = name[i+1:]
|
||||||
|
|
||||||
|
if d := strings.Index(function, "."); d != -1 {
|
||||||
|
pkg = fmt.Sprint(pkg, "/", function[:d])
|
||||||
|
function = function[d+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if o, c := strings.LastIndex(name, ".("), strings.LastIndex(name, ")."); o != -1 && c != -1 {
|
||||||
|
pkg = name[:o]
|
||||||
|
function = name[c+2:]
|
||||||
|
|
||||||
|
typer = name[o+2 : c]
|
||||||
|
if i := strings.Index(typer, "*"); i != -1 {
|
||||||
|
typer = typer[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.LastIndex(name, "."); i != -1 {
|
||||||
|
pkg = name[:i]
|
||||||
|
function = name[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package herror_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/herror"
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func trace() *herror.Stacktrace {
|
||||||
|
return herror.NewStacktrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceStepIn(f []herror.FrameFilter) *herror.Stacktrace {
|
||||||
|
return traceWithFilter(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceWithFilter(f []herror.FrameFilter) *herror.Stacktrace {
|
||||||
|
return herror.NewStacktrace(f...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStacktrace(t *testing.T) {
|
||||||
|
t.Run("new", func(t *testing.T) {
|
||||||
|
stack := trace()
|
||||||
|
|
||||||
|
expectedFrames := []herror.Frame{
|
||||||
|
herror.Frame{
|
||||||
|
Function: "TestStacktrace.func1",
|
||||||
|
},
|
||||||
|
herror.Frame{
|
||||||
|
Function: "trace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test.Equals(t, len(expectedFrames), len(stack.Frames))
|
||||||
|
for i, frame := range expectedFrames {
|
||||||
|
test.Equals(t, frame.Function, stack.Frames[i].Function)
|
||||||
|
test.Equals(t, "dev-git.sentia.com/go/kit/herror_test", stack.Frames[i].Package)
|
||||||
|
test.Equals(t, "stacktrace_test.go", stack.Frames[i].Filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filter frames", func(t *testing.T) {
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
filters []herror.FrameFilter
|
||||||
|
expected []herror.Frame
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "no filter",
|
||||||
|
expected: []herror.Frame{
|
||||||
|
herror.Frame{
|
||||||
|
Function: "TestStacktrace.func2",
|
||||||
|
},
|
||||||
|
herror.Frame{
|
||||||
|
Function: "traceStepIn",
|
||||||
|
},
|
||||||
|
herror.Frame{
|
||||||
|
Function: "traceWithFilter",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "single filter",
|
||||||
|
expected: []herror.Frame{
|
||||||
|
herror.Frame{
|
||||||
|
Function: "traceStepIn",
|
||||||
|
},
|
||||||
|
herror.Frame{
|
||||||
|
Function: "traceWithFilter",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters: []herror.FrameFilter{
|
||||||
|
func(f herror.Frame) bool {
|
||||||
|
return f.Function == "TestStacktrace.func2"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "multiple filters",
|
||||||
|
expected: []herror.Frame{
|
||||||
|
herror.Frame{
|
||||||
|
Function: "traceWithFilter",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters: []herror.FrameFilter{
|
||||||
|
func(f herror.Frame) bool {
|
||||||
|
return f.Function == "TestStacktrace.func2"
|
||||||
|
},
|
||||||
|
func(f herror.Frame) bool {
|
||||||
|
return f.Function == "traceStepIn"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
stack := traceStepIn(tc.filters)
|
||||||
|
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
test.Equals(t, len(tc.expected), len(stack.Frames))
|
||||||
|
|
||||||
|
for i, frame := range tc.expected {
|
||||||
|
test.Equals(t, frame.Function, stack.Frames[i].Function)
|
||||||
|
test.Equals(t, "dev-git.sentia.com/go/kit/herror_test", stack.Frames[i].Package)
|
||||||
|
test.Equals(t, "stacktrace_test.go", stack.Frames[i].Filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrame(t *testing.T) {
|
||||||
|
t.Run("new", func(t *testing.T) {
|
||||||
|
f := func() herror.Frame {
|
||||||
|
pc := make([]uintptr, 1)
|
||||||
|
n := runtime.Callers(0, pc)
|
||||||
|
test.Assert(t, n == 1, "expected available pcs")
|
||||||
|
|
||||||
|
frames := runtime.CallersFrames(pc)
|
||||||
|
runtimeframe, _ := frames.Next()
|
||||||
|
return herror.NewFrame(runtimeframe)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := f()
|
||||||
|
test.Equals(t, "Callers", frame.Function)
|
||||||
|
test.Equals(t, "runtime", frame.Package)
|
||||||
|
test.Equals(t, "extern.go", frame.Filename)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package log_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggerTestable represents a data structure for a log context
|
||||||
|
type LoggerTestable struct {
|
||||||
|
TestKey string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LoggerTestable) ContextName() string { return "test" }
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
var buff bytes.Buffer
|
||||||
|
var logger log.Logger
|
||||||
|
|
||||||
|
logger = log.NewLogger(&buff)
|
||||||
|
|
||||||
|
// Please ignore the following line, it was added to allow better
|
||||||
|
// assertion of the results when logging.
|
||||||
|
logger = logger.AddContext("time", "-")
|
||||||
|
|
||||||
|
logger = log.Add(logger, LoggerTestable{
|
||||||
|
TestKey: "value",
|
||||||
|
})
|
||||||
|
logger.Info("this is an example.")
|
||||||
|
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
// Output: {"message":"this is an example.","test":{"key":"value"},"time":"-"}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
kitlog "github.com/go-kit/kit/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoKit struct {
|
||||||
|
debugEnabled bool
|
||||||
|
info kitlog.Logger
|
||||||
|
debug kitlog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func caller(depth int) kitlog.Valuer {
|
||||||
|
return func() interface{} {
|
||||||
|
_, file, line, _ := runtime.Caller(depth)
|
||||||
|
return file + ":" + strconv.Itoa(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGoKitLogger(logWriter io.Writer) Logger {
|
||||||
|
w := kitlog.NewSyncWriter(logWriter)
|
||||||
|
t := kitlog.TimestampFormat(time.Now, time.RFC3339)
|
||||||
|
info := kitlog.With(kitlog.NewJSONLogger(w), "time", t)
|
||||||
|
debug := kitlog.With(info, "debug", true, "caller", caller(4))
|
||||||
|
|
||||||
|
return &GoKit{
|
||||||
|
info: info,
|
||||||
|
debug: debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddContext attaches a key-value information to the log message
|
||||||
|
func (gk *GoKit) AddContext(contextKey string, contextValue interface{}) Logger {
|
||||||
|
return &GoKit{
|
||||||
|
debugEnabled: gk.debugEnabled,
|
||||||
|
info: kitlog.With(gk.info, contextKey, contextValue),
|
||||||
|
debug: kitlog.With(gk.debug, contextKey, contextValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info writes out the log message
|
||||||
|
func (gk *GoKit) Info(message string) error {
|
||||||
|
return gk.info.Log("message", normalizeString(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug writes out the log message when debug is enabled
|
||||||
|
func (gk *GoKit) Debug(message string) error {
|
||||||
|
if gk.debugEnabled {
|
||||||
|
return gk.debug.Log("message", normalizeString(message))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugEnabled sets debug flag to enable or disabled
|
||||||
|
func (gk *GoKit) DebugEnabled(enable bool) {
|
||||||
|
gk.debugEnabled = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugStatus returns whether or not debug is enabled
|
||||||
|
func (gk *GoKit) DebugStatus() bool {
|
||||||
|
return gk.debugEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeString(s string) string {
|
||||||
|
ss := strings.Fields(s)
|
||||||
|
if len(ss) == 0 {
|
||||||
|
return "(MISSING)"
|
||||||
|
}
|
||||||
|
return strings.Join(ss, " ")
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package log_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/log"
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testLogWriter struct {
|
||||||
|
Logs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogWriter() *testLogWriter {
|
||||||
|
return &testLogWriter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testLogWriter) Write(p []byte) (n int, err error) {
|
||||||
|
t.Logs = append(t.Logs, string(p))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testLogWriter) count() int {
|
||||||
|
return len(t.Logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testLogWriter) last() string {
|
||||||
|
if len(t.Logs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Logs[len(t.Logs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoKit(t *testing.T) {
|
||||||
|
t.Run("new-logger", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := log.NewLogger(&buf)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("info", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logWriter := newLogWriter()
|
||||||
|
logger := log.NewLogger(logWriter)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
test.Equals(t, 0, logWriter.count())
|
||||||
|
|
||||||
|
msg := "log this"
|
||||||
|
test.OK(t, logger.Info(msg))
|
||||||
|
test.Equals(t, 1, logWriter.count())
|
||||||
|
testLogLine(t, false, msg, logWriter.last())
|
||||||
|
|
||||||
|
msg = "log again"
|
||||||
|
test.OK(t, logger.Info(msg))
|
||||||
|
test.Equals(t, 2, logWriter.count())
|
||||||
|
testLogLine(t, false, msg, logWriter.last())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("debug", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logWriter := newLogWriter()
|
||||||
|
logger := log.NewLogger(logWriter)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
|
||||||
|
// starts with debug disabled
|
||||||
|
test.Equals(t, false, logger.DebugStatus())
|
||||||
|
|
||||||
|
msg := "log this"
|
||||||
|
logger.DebugEnabled(true)
|
||||||
|
test.Equals(t, true, logger.DebugStatus())
|
||||||
|
logger.Debug(msg)
|
||||||
|
test.Equals(t, 1, logWriter.count())
|
||||||
|
testLogLine(t, true, msg, logWriter.last())
|
||||||
|
|
||||||
|
msg = "log again"
|
||||||
|
logger.DebugEnabled(false)
|
||||||
|
test.Equals(t, false, logger.DebugStatus())
|
||||||
|
logger.Debug(msg)
|
||||||
|
test.Equals(t, 1, logWriter.count())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("normalize-string", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
missingMsg := "(MISSING)"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
context string
|
||||||
|
logMessage string
|
||||||
|
expResult string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
context: "empty string",
|
||||||
|
logMessage: "",
|
||||||
|
expResult: missingMsg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: "single whitespace",
|
||||||
|
logMessage: " ",
|
||||||
|
expResult: missingMsg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: "multiple whitespace",
|
||||||
|
logMessage: "\t ",
|
||||||
|
expResult: missingMsg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: "simple line",
|
||||||
|
logMessage: "just some text",
|
||||||
|
expResult: "just some text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: "multiline message",
|
||||||
|
logMessage: "one\ntwo\nthree",
|
||||||
|
expResult: "one two three",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Log(tc.context)
|
||||||
|
|
||||||
|
logWriter := newLogWriter()
|
||||||
|
logger := log.NewLogger(logWriter)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
logger.DebugEnabled(true)
|
||||||
|
|
||||||
|
test.OK(t, logger.Info(tc.logMessage))
|
||||||
|
testLogLine(t, false, tc.expResult, logWriter.last())
|
||||||
|
|
||||||
|
test.OK(t, logger.Debug(tc.logMessage))
|
||||||
|
testLogLine(t, true, tc.expResult, logWriter.last())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add-context", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
message string
|
||||||
|
contextKey string
|
||||||
|
contextValue interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
message: "empty key",
|
||||||
|
contextValue: "value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "empty value",
|
||||||
|
contextKey: "a key",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
message: "not empty",
|
||||||
|
contextKey: "a key",
|
||||||
|
contextValue: "value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "slice",
|
||||||
|
contextKey: "a key",
|
||||||
|
contextValue: []string{"one", "two"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "map",
|
||||||
|
contextValue: map[string]interface{}{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": 2,
|
||||||
|
"key3": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "struct",
|
||||||
|
contextValue: struct {
|
||||||
|
Key1 string
|
||||||
|
Key2 int
|
||||||
|
Key3 interface{}
|
||||||
|
key4 string
|
||||||
|
}{
|
||||||
|
Key1: "value1",
|
||||||
|
Key2: 2,
|
||||||
|
Key3: nil,
|
||||||
|
key4: "unexported",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Log(tc.message)
|
||||||
|
|
||||||
|
logWriter := newLogWriter()
|
||||||
|
logger := log.NewLogger(logWriter)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
|
||||||
|
test.OK(t, logger.AddContext(tc.contextKey, tc.contextValue).Info("log message"))
|
||||||
|
test.Equals(t, 1, logWriter.count())
|
||||||
|
testContext(t, tc.contextKey, tc.contextValue, logWriter.last())
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLogLine(t *testing.T, debug bool, message, line string) {
|
||||||
|
test.Equals(t, true, isValidJSON(line))
|
||||||
|
test.Includes(t, `"time":`, line)
|
||||||
|
test.Includes(t, fmt.Sprintf(`"message":%q`, message), line)
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
test.Includes(t, `"debug":true`, line)
|
||||||
|
test.Includes(t, `"caller":"`, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContext(t *testing.T, key string, value interface{}, line string) {
|
||||||
|
value, err := toJSON(value)
|
||||||
|
test.OK(t, err)
|
||||||
|
test.Includes(t, fmt.Sprintf(`%q:%s`, key, value), line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSON(i interface{}) (string, error) {
|
||||||
|
j, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(j), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidJSON(s string) bool {
|
||||||
|
var js map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(s), &js)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Package log implements a generic interface to log
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger represents a log implementation
|
||||||
|
type Logger interface {
|
||||||
|
AddContext(string, interface{}) Logger
|
||||||
|
Info(string) error
|
||||||
|
Debug(string) error
|
||||||
|
DebugEnabled(bool)
|
||||||
|
DebugStatus() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger returns a Logger implementation
|
||||||
|
func NewLogger(logWriter io.Writer) Logger {
|
||||||
|
return newGoKitLogger(logWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerWriter struct {
|
||||||
|
Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *loggerWriter) Write(p []byte) (n int, err error) {
|
||||||
|
var fields map[string]interface{}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(p, &fields); err != nil {
|
||||||
|
l.Logger.Info(string(p))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(fields, "time")
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if m, ok := fields["message"]; ok {
|
||||||
|
message = m.(string)
|
||||||
|
delete(fields, "message")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) == 0 {
|
||||||
|
l.Logger.Info(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Logger.AddContext("fields", fields).Info(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter returns io.Writer implementation based on a logger
|
||||||
|
func NewWriter(l Logger) io.Writer {
|
||||||
|
return &loggerWriter{l}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package log_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/log"
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewWriter(t *testing.T) {
|
||||||
|
|
||||||
|
defaultMessage := "this is a test"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
message string
|
||||||
|
expected []string
|
||||||
|
notExpected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "string input",
|
||||||
|
message: defaultMessage,
|
||||||
|
expected: []string{
|
||||||
|
fmt.Sprintf(`"message":%q`, defaultMessage),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "json map",
|
||||||
|
message: fmt.Sprintf(`{"message":%q, "custom": "value"}`, defaultMessage),
|
||||||
|
expected: []string{
|
||||||
|
fmt.Sprintf(`"message":%q`, defaultMessage),
|
||||||
|
`"fields":{`,
|
||||||
|
`"custom":"value"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "json map correct time",
|
||||||
|
message: fmt.Sprintf(`{"message":%q, "time": "value"}`, defaultMessage),
|
||||||
|
expected: []string{
|
||||||
|
fmt.Sprintf(`"message":%q`, defaultMessage),
|
||||||
|
`"time":"`,
|
||||||
|
},
|
||||||
|
notExpected: []string{
|
||||||
|
`"fields":{`,
|
||||||
|
`"time": "value"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := log.NewLogger(&buf)
|
||||||
|
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
w := log.NewWriter(logger)
|
||||||
|
w.Write([]byte(tc.message))
|
||||||
|
for _, e := range tc.expected {
|
||||||
|
test.Includes(t, e, buf.String())
|
||||||
|
}
|
||||||
|
for _, e := range tc.notExpected {
|
||||||
|
test.NotIncludes(t, e, buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog(t *testing.T) {
|
||||||
|
t.Run("new-logger", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := log.NewLogger(&buf)
|
||||||
|
test.NotZero(t, logger)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contexter ensures type is intentionally a log context
|
||||||
|
type Contexter interface {
|
||||||
|
ContextName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller represents a runtime file:line caller for log context
|
||||||
|
type Caller func() string
|
||||||
|
|
||||||
|
// ContextName returns the key for the log context
|
||||||
|
func (c Caller) ContextName() string { return "caller" }
|
||||||
|
|
||||||
|
// NewCaller returns a log context for runtime file caller with full path
|
||||||
|
func NewCaller(depth int) Caller {
|
||||||
|
return func() string {
|
||||||
|
_, file, line, _ := runtime.Caller(depth)
|
||||||
|
return file + ":" + strconv.Itoa(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a contexter interface to a Logger
|
||||||
|
func Add(l Logger, cc ...Contexter) Logger {
|
||||||
|
for _, c := range cc {
|
||||||
|
if caller, ok := c.(Caller); ok {
|
||||||
|
l = l.AddContext(c.ContextName(), caller())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l = l.AddContext(c.ContextName(), c)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package log_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/log"
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ContextA string
|
||||||
|
ContextB string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l ContextA) ContextName() string { return "context_a" }
|
||||||
|
func (l ContextB) ContextName() string { return "context_b" }
|
||||||
|
|
||||||
|
func TestLogContext(t *testing.T) {
|
||||||
|
t.Run("new caller", func(t *testing.T) {
|
||||||
|
caller := log.NewCaller(1)
|
||||||
|
s := caller()
|
||||||
|
test.Includes(t, "logctx_test.go:", s)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add context", func(t *testing.T) {
|
||||||
|
var buff bytes.Buffer
|
||||||
|
logger := log.NewLogger(&buff)
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
cc []log.Contexter
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "single context",
|
||||||
|
cc: []log.Contexter{ContextA("AA")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "multiple context",
|
||||||
|
cc: []log.Contexter{ContextA("AA"), ContextB("BB")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "with caller context",
|
||||||
|
cc: []log.Contexter{ContextA("AA"), log.NewCaller(0)},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
log.Add(logger, tc.cc...).Info("something")
|
||||||
|
for _, context := range tc.cc {
|
||||||
|
switch s := context.(type) {
|
||||||
|
case ContextA, ContextB:
|
||||||
|
test.Includes(t, fmt.Sprintf("%q:%q", s.ContextName(), s), buff.String())
|
||||||
|
|
||||||
|
case log.Caller:
|
||||||
|
file := s()
|
||||||
|
i := strings.LastIndexByte(file, ':')
|
||||||
|
test.Includes(t, fmt.Sprintf(`%q:"%s`, s.ContextName(), file[:i+1]), buff.String())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package slugify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SKIP = []*unicode.RangeTable{
|
||||||
|
unicode.Mark,
|
||||||
|
unicode.Sk,
|
||||||
|
unicode.Lm,
|
||||||
|
}
|
||||||
|
|
||||||
|
var SAFE = []*unicode.RangeTable{
|
||||||
|
unicode.Letter,
|
||||||
|
unicode.Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slugify a string. The result will only contain lowercase letters,
|
||||||
|
// digits and dashes. It will not begin or end with a dash, and it
|
||||||
|
// will not contain runs of multiple dashes.
|
||||||
|
//
|
||||||
|
// It is NOT forced into being ASCII, but may contain any Unicode
|
||||||
|
// characters, with the above restrictions.
|
||||||
|
func Slugify(text string) string {
|
||||||
|
buf := make([]rune, 0, len(text))
|
||||||
|
dash := false
|
||||||
|
for _, r := range norm.NFKD.String(text) {
|
||||||
|
switch {
|
||||||
|
case unicode.IsOneOf(SAFE, r):
|
||||||
|
buf = append(buf, unicode.ToLower(r))
|
||||||
|
dash = true
|
||||||
|
case unicode.IsOneOf(SKIP, r):
|
||||||
|
case dash:
|
||||||
|
buf = append(buf, '-')
|
||||||
|
dash = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i := len(buf) - 1; i >= 0 && buf[i] == '-' {
|
||||||
|
buf = buf[:i]
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slugifyf slugfy a formated string
|
||||||
|
func Slugifyf(format string, a ...interface{}) string {
|
||||||
|
return Slugify(fmt.Sprintf(format, a...))
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package slugify_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/slugify"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tests = []struct{ in, out string }{
|
||||||
|
{"simple test", "simple-test"},
|
||||||
|
{"I'm go developer", "i-m-go-developer"},
|
||||||
|
{"Simples código em go", "simples-codigo-em-go"},
|
||||||
|
{"日本語の手紙をテスト", "日本語の手紙をテスト"},
|
||||||
|
{"--->simple test<---", "simple-test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlugify(t *testing.T) {
|
||||||
|
for _, test := range tests {
|
||||||
|
if out := slugify.Slugify(test.in); out != test.out {
|
||||||
|
t.Errorf("%q: %q != %q", test.in, out, test.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlugifyf(t *testing.T) {
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.out, func(t *testing.T) {
|
||||||
|
if out := slugify.Slugifyf("%s", test.in); out != test.out {
|
||||||
|
t.Errorf("%q: %q != %q", test.in, out, test.out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockResponse represents a response for the mock server to serve
|
||||||
|
type MockResponse struct {
|
||||||
|
StatusCode int
|
||||||
|
Headers http.Header
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockServerProcedure struct {
|
||||||
|
URL string
|
||||||
|
HTTPMethod string
|
||||||
|
Response MockResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockRecorder provides a way to record request information from every
|
||||||
|
// successful request.
|
||||||
|
type MockRecorder interface {
|
||||||
|
Record(r *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordedRequest represents recorded structured information about each request
|
||||||
|
type recordedRequest struct {
|
||||||
|
hits int
|
||||||
|
requests []*http.Request
|
||||||
|
bodies [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAssertion represents a common assertion for requests
|
||||||
|
type MockAssertion struct {
|
||||||
|
indexes map[string]int // indexation for key
|
||||||
|
recs []recordedRequest // request catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record records request hit information
|
||||||
|
func (m *MockAssertion) Record(r *http.Request) {
|
||||||
|
k := m.index(r.RequestURI, r.Method)
|
||||||
|
|
||||||
|
b, _ := ioutil.ReadAll(r.Body)
|
||||||
|
if len(b) == 0 {
|
||||||
|
b = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if k < 0 {
|
||||||
|
m.newIndex(r.RequestURI, r.Method)
|
||||||
|
m.recs = append(m.recs, recordedRequest{
|
||||||
|
hits: 1,
|
||||||
|
requests: []*http.Request{r},
|
||||||
|
bodies: [][]byte{b},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.recs[k].hits++
|
||||||
|
m.recs[k].requests = append(m.recs[k].requests, r)
|
||||||
|
m.recs[k].bodies = append(m.recs[k].bodies, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hits returns the number of hits for a uri and method
|
||||||
|
func (m *MockAssertion) Hits(uri, method string) int {
|
||||||
|
k := m.index(uri, method)
|
||||||
|
if k < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.recs[k].hits
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns a slice of request headers
|
||||||
|
func (m *MockAssertion) Headers(uri, method string) []http.Header {
|
||||||
|
k := m.index(uri, method)
|
||||||
|
if k < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := make([]http.Header, len(m.recs[k].requests))
|
||||||
|
for i, r := range m.recs[k].requests {
|
||||||
|
|
||||||
|
// remove default headers
|
||||||
|
if _, ok := r.Header["Content-Length"]; ok {
|
||||||
|
r.Header.Del("Content-Length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := r.Header["User-Agent"]; ok {
|
||||||
|
if _, yes := equals([]string{"Go-http-client/1.1"}, v); yes {
|
||||||
|
r.Header.Del("User-Agent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := r.Header["Accept-Encoding"]; ok {
|
||||||
|
if _, yes := equals([]string{"gzip"}, v); yes {
|
||||||
|
r.Header.Del("Accept-Encoding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Header) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[i] = r.Header
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body returns request body
|
||||||
|
func (m *MockAssertion) Body(uri, method string) [][]byte {
|
||||||
|
k := m.index(uri, method)
|
||||||
|
if k < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.recs[k].bodies
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset sets all unexpected properties to their zero value
|
||||||
|
func (m *MockAssertion) Reset() error {
|
||||||
|
m.indexes = make(map[string]int)
|
||||||
|
m.recs = make([]recordedRequest, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// index indexes a key composed of the uri and method and returns the position
|
||||||
|
// for this key in a list if it was indexed before.
|
||||||
|
func (m *MockAssertion) index(uri, method string) int {
|
||||||
|
if isZero(m.indexes) {
|
||||||
|
m.indexes = make(map[string]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
k := strings.ToLower(uri + method)
|
||||||
|
|
||||||
|
if i, ok := m.indexes[k]; ok {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAssertion) newIndex(uri, method string) int {
|
||||||
|
k := strings.ToLower(uri + method)
|
||||||
|
m.indexes[k] = len(m.indexes)
|
||||||
|
return m.indexes[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockServer return a mock HTTP server to test requests
|
||||||
|
func NewMockServer(rec MockRecorder, procedures ...MockServerProcedure) *httptest.Server {
|
||||||
|
var handler http.Handler
|
||||||
|
|
||||||
|
handler = http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
for _, proc := range procedures {
|
||||||
|
|
||||||
|
if proc.URL == r.URL.RequestURI() && proc.HTTPMethod == r.Method {
|
||||||
|
|
||||||
|
headers := w.Header()
|
||||||
|
for hkey, hvalue := range proc.Response.Headers {
|
||||||
|
headers[hkey] = hvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
code := proc.Response.StatusCode
|
||||||
|
if code == 0 {
|
||||||
|
code = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(code)
|
||||||
|
w.Write(proc.Response.Body)
|
||||||
|
|
||||||
|
if rec != nil {
|
||||||
|
rec.Record(r)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
return httptest.NewServer(handler)
|
||||||
|
}
|
|
@ -0,0 +1,283 @@
|
||||||
|
package test_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPMock(t *testing.T) {
|
||||||
|
|
||||||
|
procs := []test.MockServerProcedure{
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/",
|
||||||
|
HTTPMethod: "GET",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
Body: []byte("getRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/",
|
||||||
|
HTTPMethod: "POST",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
Body: []byte("postRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/get/header",
|
||||||
|
HTTPMethod: "GET",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusAccepted,
|
||||||
|
Headers: http.Header{
|
||||||
|
"some-key": []string{"some-value"},
|
||||||
|
},
|
||||||
|
Body: []byte("getResponseHeader"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/get/auth",
|
||||||
|
HTTPMethod: "GET",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
Body: []byte("getRootAuth"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/my_account",
|
||||||
|
HTTPMethod: "GET",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
Body: []byte("getAccount"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test.MockServerProcedure{
|
||||||
|
URL: "/my_account.json",
|
||||||
|
HTTPMethod: "GET",
|
||||||
|
Response: test.MockResponse{
|
||||||
|
Body: []byte("getAccountJSON"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var record test.MockAssertion
|
||||||
|
testMockServer := test.NewMockServer(&record, procs...)
|
||||||
|
|
||||||
|
type mockRequest struct {
|
||||||
|
uri string
|
||||||
|
method string
|
||||||
|
user, password string
|
||||||
|
header http.Header
|
||||||
|
body []byte
|
||||||
|
hits int
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical := textproto.CanonicalMIMEHeaderKey
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
m string
|
||||||
|
request mockRequest
|
||||||
|
response test.MockResponse
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
m: "method get root path",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodGet,
|
||||||
|
hits: 2,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("getRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "method get root path with headers",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodGet,
|
||||||
|
header: http.Header{
|
||||||
|
canonical("input-header-key"): []string{"Just the Value"},
|
||||||
|
},
|
||||||
|
hits: 2,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("getRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "method get root path with body",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodGet,
|
||||||
|
body: []byte("input"),
|
||||||
|
hits: 2,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("getRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "method get root path with headers and body",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodGet,
|
||||||
|
header: http.Header{
|
||||||
|
canonical("input-header-key"): []string{"Just the Value"},
|
||||||
|
},
|
||||||
|
body: []byte("input"),
|
||||||
|
hits: 2,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("getRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "method post root path",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodPost,
|
||||||
|
hits: 2,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("postRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "method post root path with basic authentication",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/",
|
||||||
|
method: http.MethodPost,
|
||||||
|
user: "my-user",
|
||||||
|
password: "my-password",
|
||||||
|
hits: 1,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: []byte("postRoot"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
m: "unmatched uri path",
|
||||||
|
request: mockRequest{
|
||||||
|
uri: "/unmatched",
|
||||||
|
method: http.MethodGet,
|
||||||
|
hits: 0,
|
||||||
|
},
|
||||||
|
response: test.MockResponse{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: []byte{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.m, func(t *testing.T) {
|
||||||
|
test.OK(t, record.Reset())
|
||||||
|
|
||||||
|
for _ = range make([]int, tc.request.hits) {
|
||||||
|
url, errU := url.Parse(testMockServer.URL + tc.request.uri)
|
||||||
|
test.OK(t, errU)
|
||||||
|
|
||||||
|
req, errReq := http.NewRequest(
|
||||||
|
tc.request.method,
|
||||||
|
url.String(),
|
||||||
|
bytes.NewReader(tc.request.body),
|
||||||
|
)
|
||||||
|
test.OK(t, errReq)
|
||||||
|
|
||||||
|
for k, v := range tc.request.header {
|
||||||
|
req.Header[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// testing authentication in the request
|
||||||
|
if len(tc.request.user) > 0 || len(tc.request.password) > 0 {
|
||||||
|
req.SetBasicAuth(tc.request.user, tc.request.password)
|
||||||
|
|
||||||
|
if tc.request.header == nil {
|
||||||
|
tc.request.header = make(http.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := tc.request.user + ":" + tc.request.password
|
||||||
|
tc.request.header["Authorization"] = []string{
|
||||||
|
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth)))}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := new(http.Client)
|
||||||
|
resp, errResp := client.Do(req)
|
||||||
|
test.OK(t, errResp)
|
||||||
|
|
||||||
|
actualBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
test.OK(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
test.Equals(t, tc.response.StatusCode, resp.StatusCode)
|
||||||
|
test.Equals(t, tc.response.Body, actualBody)
|
||||||
|
}
|
||||||
|
test.Equals(t, tc.request.hits, record.Hits(tc.request.uri, tc.request.method))
|
||||||
|
|
||||||
|
// assert if all request had the correct header
|
||||||
|
for _, h := range record.Headers(tc.request.uri, tc.request.method) {
|
||||||
|
test.Equals(t, tc.request.header, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert if all request had the correct body
|
||||||
|
for _, b := range record.Body(tc.request.uri, tc.request.method) {
|
||||||
|
test.Equals(t, tc.request.body, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMockAssertion_Hits() {
|
||||||
|
var record test.MockAssertion
|
||||||
|
uri := "/"
|
||||||
|
|
||||||
|
server := test.NewMockServer(&record, test.MockServerProcedure{
|
||||||
|
URL: uri,
|
||||||
|
HTTPMethod: http.MethodGet,
|
||||||
|
})
|
||||||
|
|
||||||
|
http.Get(server.URL)
|
||||||
|
|
||||||
|
fmt.Println(record.Hits(uri, http.MethodGet))
|
||||||
|
// Output: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMockAssertion_Headers() {
|
||||||
|
var record test.MockAssertion
|
||||||
|
uri := "/"
|
||||||
|
|
||||||
|
server := test.NewMockServer(&record, test.MockServerProcedure{
|
||||||
|
URL: uri,
|
||||||
|
HTTPMethod: http.MethodPost,
|
||||||
|
})
|
||||||
|
|
||||||
|
http.Post(server.URL, "application/json", nil)
|
||||||
|
|
||||||
|
fmt.Println(record.Headers(uri, http.MethodPost))
|
||||||
|
// Output: [map[Content-Type:[application/json]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMockAssertion_Body() {
|
||||||
|
var record test.MockAssertion
|
||||||
|
uri := "/"
|
||||||
|
|
||||||
|
server := test.NewMockServer(&record, test.MockServerProcedure{
|
||||||
|
URL: uri,
|
||||||
|
HTTPMethod: http.MethodPost,
|
||||||
|
})
|
||||||
|
|
||||||
|
http.Post(server.URL, "text/plain", bytes.NewBufferString("hi there"))
|
||||||
|
|
||||||
|
for _, b := range record.Body(uri, http.MethodPost) {
|
||||||
|
fmt.Println(string(b))
|
||||||
|
}
|
||||||
|
// Output: hi there
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert fails the test if the condition is false.
|
||||||
|
func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
|
||||||
|
if !condition {
|
||||||
|
b := bytes.NewBufferString("\t" + msg + "\n")
|
||||||
|
fmt.Fprintln(b, v...)
|
||||||
|
print(b)
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK fails the test if an err is not nil.
|
||||||
|
func OK(tb testing.TB, err error) {
|
||||||
|
if err != nil {
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tUnexpected error: %v", err)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNil fails the test if anything is nil.
|
||||||
|
func NotNil(tb testing.TB, anything interface{}) {
|
||||||
|
if isNil(anything) {
|
||||||
|
print(bytes.NewBufferString("\tExpected non-nil value"))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nil fails the test if something is NOT nil.
|
||||||
|
func Nil(tb testing.TB, something interface{}) {
|
||||||
|
if !isNil(something) {
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tExpected value to be nil\n\n\tgot: %#v", something)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals fails the test if exp is not equal to act.
|
||||||
|
func Equals(tb testing.TB, exp, act interface{}) {
|
||||||
|
if b, ok := equals(exp, act); !ok {
|
||||||
|
print(b)
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func equals(exp, act interface{}) (b *bytes.Buffer, ok bool) {
|
||||||
|
b = new(bytes.Buffer)
|
||||||
|
fmt.Fprintf(b, "\texp: %s\n\n\tgot: %s", stringer(exp), stringer(act))
|
||||||
|
return b, reflect.DeepEqual(exp, act)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Includes fails if expected string is NOT included in the actual string
|
||||||
|
func Includes(tb testing.TB, exp string, act ...string) {
|
||||||
|
for _, a := range act {
|
||||||
|
if strings.Index(a, exp) >= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tExpected to include: %s\n\n\tgot: %s", exp, act)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotIncludes fails if expected string is included in the actual string
|
||||||
|
func NotIncludes(tb testing.TB, exp string, act ...string) {
|
||||||
|
for _, a := range act {
|
||||||
|
if strings.Index(a, exp) >= 0 {
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tNOT expected to include: %#v\n\n\tgot: %#v", exp, act)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncludesI fails if expected string is NOT included in the actuall string (ignore case)
|
||||||
|
func IncludesI(tb testing.TB, exp string, act ...string) {
|
||||||
|
for _, a := range act {
|
||||||
|
if strings.Index(strings.ToLower(a), strings.ToLower(exp)) >= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tExpected to include: %s\n\n\tgot: %s", exp, act)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncludesSlice fails if all of expected items is NOT included in the actual slice
|
||||||
|
func IncludesSlice(tb testing.TB, exp, act interface{}) {
|
||||||
|
if reflect.ValueOf(exp).Kind() != reflect.Slice {
|
||||||
|
panic("IncludesSlice requires a expected slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.ValueOf(act).Kind() != reflect.Slice {
|
||||||
|
panic("IncludesSlice requires a actual slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
expSlice := reflect.ValueOf(exp)
|
||||||
|
actSlice := reflect.ValueOf(act)
|
||||||
|
|
||||||
|
expLen := expSlice.Len()
|
||||||
|
actLen := actSlice.Len()
|
||||||
|
|
||||||
|
if expLen <= actLen {
|
||||||
|
var score int
|
||||||
|
for idxA := 0; idxA < actLen; idxA++ {
|
||||||
|
for idxE := 0; idxE < expLen; idxE++ {
|
||||||
|
if reflect.DeepEqual(expSlice.Index(idxE).Interface(), actSlice.Index(idxA).Interface()) {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if score == expLen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(bytes.NewBufferString(
|
||||||
|
fmt.Sprintf("\tExpected to all items to be included: %+v\n\n\tIn: %+v", exp, act)))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncludesMap fails if all of expected map entries are NOT included in the actuall map
|
||||||
|
func IncludesMap(tb testing.TB, exp, act interface{}) {
|
||||||
|
if b, ok := includesMap(exp, act); !ok {
|
||||||
|
print(b)
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func includesMap(exp, act interface{}) (b *bytes.Buffer, ok bool) {
|
||||||
|
if reflect.ValueOf(exp).Kind() != reflect.Map {
|
||||||
|
panic("IncludesMap requires a expected map")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.ValueOf(act).Kind() != reflect.Map {
|
||||||
|
panic("IncludesMap requires a actual map")
|
||||||
|
}
|
||||||
|
|
||||||
|
expMap := reflect.ValueOf(exp)
|
||||||
|
actMap := reflect.ValueOf(act)
|
||||||
|
|
||||||
|
expLen := len(expMap.MapKeys())
|
||||||
|
actLen := len(actMap.MapKeys())
|
||||||
|
|
||||||
|
if expLen <= actLen {
|
||||||
|
var score int
|
||||||
|
for _, actKey := range actMap.MapKeys() {
|
||||||
|
for _, expKey := range expMap.MapKeys() {
|
||||||
|
if reflect.DeepEqual(expKey.Interface(), actKey.Interface()) &&
|
||||||
|
reflect.DeepEqual(expMap.MapIndex(expKey).Interface(), actMap.MapIndex(actKey).Interface()) {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if score == expLen {
|
||||||
|
return b, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(b, "\tExpected to all items to be included: %+v\n\n\tIn: %+v", exp, act)
|
||||||
|
return b, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero fails the test if anything is NOT nil.
|
||||||
|
func Zero(tb testing.TB, anything interface{}) {
|
||||||
|
if !isZero(anything) {
|
||||||
|
print(bytes.NewBufferString("\tExpected zero value"))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotZero fails the test if anything is NOT nil.
|
||||||
|
func NotZero(tb testing.TB, anything interface{}) {
|
||||||
|
if isZero(anything) {
|
||||||
|
print(bytes.NewBufferString("\tExpected non-zero value"))
|
||||||
|
tb.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(anything interface{}) bool {
|
||||||
|
refZero := reflect.Zero(reflect.ValueOf(anything).Type())
|
||||||
|
return reflect.DeepEqual(refZero.Interface(), anything)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNil(anything interface{}) bool {
|
||||||
|
return reflect.DeepEqual(reflect.ValueOf(nil), reflect.ValueOf(anything)) ||
|
||||||
|
reflect.ValueOf(anything).IsNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(b *bytes.Buffer) {
|
||||||
|
_, file, line, _ := runtime.Caller(2)
|
||||||
|
fmt.Printf("\033[31m%s:%d:\n\n%s\033[39m\n\n",
|
||||||
|
filepath.Base(file), line, b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringer(a interface{}) string {
|
||||||
|
switch s := a.(type) {
|
||||||
|
case string:
|
||||||
|
return s
|
||||||
|
case []byte:
|
||||||
|
return string(s)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%#v", s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package test_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dev-git.sentia.com/go/kit/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTest(t *testing.T) {
|
||||||
|
t.Run("assert", func(t *testing.T) {
|
||||||
|
condition := true
|
||||||
|
test.Assert(t, condition, "expected condition to be true")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
var condition error
|
||||||
|
test.Assert(t, condition == nil, "expected condition to be true")
|
||||||
|
test.OK(t, condition)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not-nil", func(t *testing.T) {
|
||||||
|
var condition error
|
||||||
|
condition = errors.New("some error here")
|
||||||
|
test.NotNil(t, condition)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var condition error
|
||||||
|
test.Nil(t, condition)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("equals", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
message string
|
||||||
|
expected interface{}
|
||||||
|
result interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
message: "when expected is zero value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected is nil",
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are struct",
|
||||||
|
expected: struct{ test string }{"testing"},
|
||||||
|
result: struct{ test string }{"testing"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are strings",
|
||||||
|
expected: "testing",
|
||||||
|
result: "testing",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Log(tc.message)
|
||||||
|
{
|
||||||
|
test.Equals(t, tc.expected, tc.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not-zero", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
message string
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
message: "when expected and result are struct",
|
||||||
|
expected: struct{ test string }{"testing"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are strings",
|
||||||
|
expected: "testing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are integers",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Log(tc.message)
|
||||||
|
{
|
||||||
|
test.NotZero(t, tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
message string
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
message: "when expected and result are struct",
|
||||||
|
expected: struct{ test string }{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are strings",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "when expected and result are integers",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Log(tc.message)
|
||||||
|
{
|
||||||
|
test.Zero(t, tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("includes", func(t *testing.T) {
|
||||||
|
result := "The quick brown fox jumps over the lazy dog"
|
||||||
|
expected := "jumps"
|
||||||
|
test.Includes(t, expected, result)
|
||||||
|
|
||||||
|
resultList := []string{"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"}
|
||||||
|
test.Includes(t, expected, resultList...)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("includes-i", func(t *testing.T) {
|
||||||
|
result := "The quick brown fox jumps over the lazy dog"
|
||||||
|
expected := "JUMPS"
|
||||||
|
test.IncludesI(t, expected, result)
|
||||||
|
|
||||||
|
resultList := []string{"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"}
|
||||||
|
test.IncludesI(t, expected, resultList...)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not-includes", func(t *testing.T) {
|
||||||
|
result := "The quick brown fox jumps over the lazy dog"
|
||||||
|
expected := "hippo"
|
||||||
|
test.NotIncludes(t, expected, result)
|
||||||
|
|
||||||
|
resultList := []string{"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"}
|
||||||
|
test.NotIncludes(t, expected, resultList...)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("includes-slice", func(t *testing.T) {
|
||||||
|
expected := []string{"B"}
|
||||||
|
original := []string{"A", "B", "C"}
|
||||||
|
test.IncludesSlice(t, expected, original)
|
||||||
|
|
||||||
|
expectedI := []int{5}
|
||||||
|
originalI := []int{1, 2, 3, 4, 5, 6, 7}
|
||||||
|
test.IncludesSlice(t, expectedI, originalI)
|
||||||
|
|
||||||
|
expectedE := []interface{}{5, "B"}
|
||||||
|
originalE := []interface{}{1, 2, 3, 4, 5, 6, 7, "A", "B", "C"}
|
||||||
|
test.IncludesSlice(t, expectedE, originalE)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("includes-map", func(t *testing.T) {
|
||||||
|
expected := map[string]string{"B": "B"}
|
||||||
|
original := map[string]string{
|
||||||
|
"A": "A",
|
||||||
|
"B": "B",
|
||||||
|
"C": "C",
|
||||||
|
}
|
||||||
|
test.IncludesMap(t, expected, original)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue