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:
Erik Winter 2019-08-29 14:51:20 +02:00
commit 38a0889c15
26 changed files with 2229 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
debug.*
coverage/coverage.*
coverage/*cover.out

13
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,13 @@
image: golang:1.12
stages:
- test
variables:
GO111MODULE: "on"
test:
stage: test
script:
- cd ${CI_PROJECT_DIR}
- make

50
Makefile Normal file
View File

@ -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

View File

@ -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.

5
coverage/merge.awk Normal file
View File

@ -0,0 +1,5 @@
{
if (last != $1)
print $0
last = $1
}

View File

@ -0,0 +1,4 @@
{
if ($0 ~ /^total\:/)
print "coverage: " $3 " of statements";
}

11
go.mod Normal file
View File

@ -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
)

12
go.sum Normal file
View File

@ -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=

View File

@ -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
}

155
herror/herror.go Normal file
View File

@ -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,
})
}

150
herror/herror_test.go Normal file
View File

@ -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
// }
}

151
herror/stacktrace.go Normal file
View File

@ -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
}

130
herror/stacktrace_test.go Normal file
View File

@ -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)
})
}

View File

@ -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":"-"}
}

76
log/gokit.go Normal file
View File

@ -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, " ")
}

238
log/gokit_test.go Normal file
View File

@ -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
}

55
log/log.go Normal file
View File

@ -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}
}

72
log/log_test.go Normal file
View File

@ -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)
})
}

37
log/logctx.go Normal file
View File

@ -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
}

66
log/logctx_test.go Normal file
View File

@ -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())
}
}
})
}
})
}

50
slugify/slugify.go Normal file
View File

@ -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...))
}

33
slugify/slugify_test.go Normal file
View File

@ -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)
}
})
}
}

187
test/http_mock.go Normal file
View File

@ -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)
}

283
test/http_mock_test.go Normal file
View File

@ -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
}

219
test/test.go Normal file
View File

@ -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)
}
}

164
test/test_test.go Normal file
View File

@ -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)
})
}