Merge branch 'hh-86-add-packages' into 'master'
HH-86: add log, slugify, herror and test packages See merge request go/kit!1
This commit is contained in:
commit
38a0889c15
|
@ -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.
|
||||
|
|
|
@ -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