From 6859ad5661ee8950b58351773b76480dcebecad0 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 10 Jan 2020 16:45:26 +0100 Subject: [PATCH] Migrated audit into own repository Signed-off-by: Frank Mueller --- .gitignore | 1 + CHANGELOG.md | 7 + LICENSE | 42 +- Makefile | 44 ++ README.md | 26 ++ asserts/asserts.go | 502 +++++++++++++++++++++ asserts/asserts_test.go | 637 +++++++++++++++++++++++++++ asserts/doc.go | 24 ++ asserts/failer.go | 420 ++++++++++++++++++ asserts/printer.go | 229 ++++++++++ asserts/tester.go | 261 +++++++++++ capture/capture.go | 112 +++++ capture/capture_test.go | 97 +++++ capture/doc.go | 24 ++ environments/doc.go | 18 + environments/environments.go | 162 +++++++ environments/environments_test.go | 115 +++++ environments/web.go | 421 ++++++++++++++++++ environments/web_test.go | 154 +++++++ generators/doc.go | 14 + generators/generators.go | 696 ++++++++++++++++++++++++++++++ generators/generators_test.go | 378 ++++++++++++++++ go.mod | 3 + 23 files changed, 4366 insertions(+), 21 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 asserts/asserts.go create mode 100644 asserts/asserts_test.go create mode 100644 asserts/doc.go create mode 100644 asserts/failer.go create mode 100644 asserts/printer.go create mode 100644 asserts/tester.go create mode 100644 capture/capture.go create mode 100644 capture/capture_test.go create mode 100644 capture/doc.go create mode 100644 environments/doc.go create mode 100644 environments/environments.go create mode 100644 environments/environments_test.go create mode 100644 environments/web.go create mode 100644 environments/web_test.go create mode 100644 generators/doc.go create mode 100644 generators/generators.go create mode 100644 generators/generators_test.go create mode 100644 go.mod diff --git a/.gitignore b/.gitignore index 66fd13c..e560bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +coverage.txt # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6a1d200 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [0.3.0] - 2020-01-10 + +### Changed + +- Extracted from Tideland Go Library as part of split diff --git a/LICENSE b/LICENSE index d6e961c..37f33d2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,29 @@ BSD 3-Clause License -Copyright (c) 2020, Tideland +Copyright (c) 2009-2020 Frank Mueller / Tideland / Oldenburg / Germany All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +- Neither the name of Tideland nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..978c99c --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +SHELL=/bin/bash +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOLINT=golangci-lint + +GO111MODULE=on + + +.PHONY: download +download: ## Download module dependencies + go mod download + + +.PHONY: build +build: ## Build the library + $(GOBUILD) -v ./... + + +.PHONY: lint +lint: ## Run the linter + $(GOLINT) run ./... + + +.PHONY: test +test: ## Run all the tests + echo 'mode: atomic' > coverage.txt && $(GOTEST) -v -race -covermode=atomic -coverprofile=coverage.txt -timeout=30s ./... + +.PHONY: ci +ci: lint test ## Run all the tests and code checks + + +.PHONY: clean +clean: ## Clean + $(GOCLEAN) + + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' + +.DEFAULT_GOAL := build diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f8c8cc --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Tideland Go Audit + +[![GitHub release](https://img.shields.io/github/release/tideland/go-audit.svg)](https://github.com/tideland/go-audit) +[![GitHub license](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://raw.githubusercontent.com/tideland/go-audit/master/LICENSE) +[![GoDoc](https://godoc.org/tideland.dev/go/audit?status.svg)](https://godoc.org/tideland.dev/go/audit) +[![Sourcegraph](https://sourcegraph.com/github.com/tideland/go-audit/-/badge.svg)](https://sourcegraph.com/github.com/tideland/go-audit?badge) +[![Go Report Card](https://goreportcard.com/badge/github.com/tideland/go-audit)](https://goreportcard.com/report/tideland.dev/go-audit) + +## Description + +**Tideland Go Audit** provides helpful packages to support testing. + +* `asserts` provides routines for assertions helpful in tests and validation +* `capture` allows capturing of STDOUT and STDERR +* `environments` provides setting of environment variables, creation of temporary directories, and running web servers for tests +* `generators` simplifies generation of test data; with a fixed random on demand even repeatable + +I hope you like it. ;) + +## Contributors + +- Frank Mueller (https://github.com/themue / https://github.com/tideland / https://tideland.dev) + +## License + +**Tideland Go Audit** is distributed under the terms of the BSD 3-Clause license. diff --git a/asserts/asserts.go b/asserts/asserts.go new file mode 100644 index 0000000..af072ff --- /dev/null +++ b/asserts/asserts.go @@ -0,0 +1,502 @@ +// Tideland Go Audit - Asserts +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package asserts // import "tideland.dev/go/audit/asserts" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "fmt" + "strings" + "sync" + "time" +) + +//-------------------- +// ASSERTS +//-------------------- + +// Asserts provides a number of convenient test methods. +type Asserts struct { + Tester + failer Failer +} + +// New creates a new Asserts instance. +func New(f Failer) *Asserts { + return &Asserts{ + failer: f, + } +} + +// SetPrinter sets a new Printer used for the output of failing +// tests or logging. The current one is returned, e.g. for a +// later restoring. +func (a *Asserts) SetPrinter(printer Printer) Printer { + return a.failer.SetPrinter(printer) +} + +// SetFailable allows to change the failable possibly used inside +// a failer. This way a testing.T of a sub-test can be injected. A +// restore function is returned. +// +// t.Run(name, func(t *testing.T)) { +// defer assert.SetFailable(t)() +// ... +// }) +// +// So the returned restorer function will be called when +// leaving the sub-test. +func (a *Asserts) SetFailable(f Failable) func() { + tf, ok := a.failer.(*testingFailer) + if !ok { + // Nothing to do. + return func() {} + } + // It's a test assertion. + old := tf.failable + tf.failable = f + return func() { + tf.failable = old + } +} + +// IncrCallstackOffset allows test libraries using the audit +// package internally to adjust the callstack offset. This +// way test output shows the correct location. Deferring +// the returned function restores the former offset. +func (a *Asserts) IncrCallstackOffset() func() { + return a.failer.IncrCallstackOffset() +} + +// Logf can be used to display helpful information during testing. +func (a *Asserts) Logf(format string, args ...interface{}) { + a.failer.Logf(format, args...) +} + +// True tests if obtained is true. +func (a *Asserts) True(obtained bool, msgs ...string) bool { + if !a.IsTrue(obtained) { + return a.failer.Fail(True, obtained, true, msgs...) + } + return true +} + +// False tests if obtained is false. +func (a *Asserts) False(obtained bool, msgs ...string) bool { + if a.IsTrue(obtained) { + return a.failer.Fail(False, obtained, false, msgs...) + } + return true +} + +// Nil tests if obtained is nil. +func (a *Asserts) Nil(obtained interface{}, msgs ...string) bool { + if !a.IsNil(obtained) { + return a.failer.Fail(Nil, obtained, nil, msgs...) + } + return true +} + +// NotNil tests if obtained is not nil. +func (a *Asserts) NotNil(obtained interface{}, msgs ...string) bool { + if a.IsNil(obtained) { + return a.failer.Fail(NotNil, obtained, nil, msgs...) + } + return true +} + +// Equal tests if obtained and expected are equal. +func (a *Asserts) Equal(obtained, expected interface{}, msgs ...string) bool { + if !a.IsEqual(obtained, expected) { + return a.failer.Fail(Equal, obtained, expected, msgs...) + } + return true +} + +// Different tests if obtained and expected are different. +func (a *Asserts) Different(obtained, expected interface{}, msgs ...string) bool { + if a.IsEqual(obtained, expected) { + return a.failer.Fail(Different, obtained, expected, msgs...) + } + return true +} + +// NoError tests if the obtained error is nil. +func (a *Asserts) NoError(obtained error, msgs ...string) bool { + if !a.IsNil(obtained) { + return a.failer.Fail(NoError, obtained, nil, msgs...) + } + return true +} + +// ErrorMatch tests if the obtained error as string matches a +// regular expression. +func (a *Asserts) ErrorMatch(obtained error, regex string, msgs ...string) bool { + if obtained == nil { + return a.failer.Fail(ErrorMatch, nil, regex, "error is nil") + } + matches, err := a.IsMatching(obtained.Error(), regex) + if err != nil { + return a.failer.Fail(ErrorMatch, obtained, regex, "can't compile regex: "+err.Error()) + } + if !matches { + return a.failer.Fail(ErrorMatch, obtained, regex, msgs...) + } + return true +} + +// ErrorContains tests if the obtained error contains a given string. +func (a *Asserts) ErrorContains(obtained error, part string, msgs ...string) bool { + if !a.IsSubstring(part, obtained.Error()) { + return a.failer.Fail(ErrorContains, obtained, part, msgs...) + } + return true +} + +// Contents tests if the obtained data is part of the expected +// string, array, or slice. +func (a *Asserts) Contents(part, full interface{}, msgs ...string) bool { + contains, err := a.Contains(part, full) + if err != nil { + return a.failer.Fail(Contents, part, full, "type missmatch: "+err.Error()) + } + if !contains { + return a.failer.Fail(Contents, part, full, msgs...) + } + return true +} + +// NotContents tests if the obtained data is not part of the expected +// string, array, or slice. +func (a *Asserts) NotContents(part, full interface{}, msgs ...string) bool { + contains, err := a.Contains(part, full) + if err != nil { + return a.failer.Fail(Contents, part, full, "type missmatch: "+err.Error()) + } + if contains { + return a.failer.Fail(Contents, part, full, msgs...) + } + return true +} + +// About tests if obtained and expected are near to each other +// (within the given extent). +func (a *Asserts) About(obtained, expected, extent float64, msgs ...string) bool { + if !a.IsAbout(obtained, expected, extent) { + return a.failer.Fail(About, obtained, expected, msgs...) + } + return true +} + +// Range tests if obtained is larger or equal low and lower or +// equal high. Allowed are byte, int and float64 for numbers, runes, +// strings, times, and duration. In case of obtained arrays, +// slices, and maps low and high have to be ints for testing +// the length. +func (a *Asserts) Range(obtained, low, high interface{}, msgs ...string) bool { + expected := &lowHigh{low, high} + inRange, err := a.IsInRange(obtained, low, high) + if err != nil { + return a.failer.Fail(Range, obtained, expected, "type missmatch: "+err.Error()) + } + if !inRange { + return a.failer.Fail(Range, obtained, expected, msgs...) + } + return true +} + +// Substring tests if obtained is a substring of the full string. +func (a *Asserts) Substring(obtained, full string, msgs ...string) bool { + if !a.IsSubstring(obtained, full) { + return a.failer.Fail(Substring, obtained, full, msgs...) + } + return true +} + +// Case tests if obtained string is uppercase or lowercase. +func (a *Asserts) Case(obtained string, upperCase bool, msgs ...string) bool { + if !a.IsCase(obtained, upperCase) { + if upperCase { + return a.failer.Fail(Case, obtained, strings.ToUpper(obtained), msgs...) + } + return a.failer.Fail(Case, obtained, strings.ToLower(obtained), msgs...) + } + return true +} + +// Match tests if the obtained string matches a regular expression. +func (a *Asserts) Match(obtained, regex string, msgs ...string) bool { + matches, err := a.IsMatching(obtained, regex) + if err != nil { + return a.failer.Fail(Match, obtained, regex, "can't compile regex: "+err.Error()) + } + if !matches { + return a.failer.Fail(Match, obtained, regex, msgs...) + } + return true +} + +// Implementor tests if obtained implements the expected +// interface variable pointer. +func (a *Asserts) Implementor(obtained, expected interface{}, msgs ...string) bool { + implements, err := a.IsImplementor(obtained, expected) + if err != nil { + return a.failer.Fail(Implementor, obtained, expected, err.Error()) + } + if !implements { + return a.failer.Fail(Implementor, obtained, expected, msgs...) + } + return implements +} + +// Assignable tests if the types of expected and obtained are assignable. +func (a *Asserts) Assignable(obtained, expected interface{}, msgs ...string) bool { + if !a.IsAssignable(obtained, expected) { + return a.failer.Fail(Assignable, obtained, expected, msgs...) + } + return true +} + +// Unassignable tests if the types of expected and obtained are +// not assignable. +func (a *Asserts) Unassignable(obtained, expected interface{}, msgs ...string) bool { + if a.IsAssignable(obtained, expected) { + return a.failer.Fail(Unassignable, obtained, expected, msgs...) + } + return true +} + +// Empty tests if the len of the obtained string, array, slice +// map, or channel is 0. +func (a *Asserts) Empty(obtained interface{}, msgs ...string) bool { + length, err := a.Len(obtained) + if err != nil { + return a.failer.Fail(Empty, ValueDescription(obtained), 0, err.Error()) + } + if length > 0 { + return a.failer.Fail(Empty, length, 0, msgs...) + + } + return true +} + +// NotEmpty tests if the len of the obtained string, array, slice +// map, or channel is greater than 0. +func (a *Asserts) NotEmpty(obtained interface{}, msgs ...string) bool { + length, err := a.Len(obtained) + if err != nil { + return a.failer.Fail(NotEmpty, ValueDescription(obtained), 0, err.Error()) + } + if length == 0 { + return a.failer.Fail(NotEmpty, length, 0, msgs...) + + } + return true +} + +// Length tests if the len of the obtained string, array, slice +// map, or channel is equal to the expected one. +func (a *Asserts) Length(obtained interface{}, expected int, msgs ...string) bool { + length, err := a.Len(obtained) + if err != nil { + return a.failer.Fail(Length, ValueDescription(obtained), expected, err.Error()) + } + if length != expected { + return a.failer.Fail(Length, length, expected, msgs...) + } + return true +} + +// Panics checks if the passed function panics. +func (a *Asserts) Panics(pf func(), msgs ...string) bool { + if !a.HasPanic(pf) { + return a.failer.Fail(Panics, ValueDescription(pf), nil, msgs...) + } + return true +} + +// PathExists checks if the passed path or file exists. +func (a *Asserts) PathExists(obtained string, msgs ...string) bool { + valid, err := a.IsValidPath(obtained) + if err != nil { + return a.failer.Fail(PathExists, obtained, true, err.Error()) + } + if !valid { + return a.failer.Fail(PathExists, obtained, true, msgs...) + } + return true +} + +// Wait receives a signal from a channel and compares it to the +// expired value. Assert also fails on timeout. +func (a *Asserts) Wait( + sigc <-chan interface{}, + expected interface{}, + timeout time.Duration, + msgs ...string, +) bool { + select { + case obtained := <-sigc: + if !a.IsEqual(obtained, expected) { + return a.failer.Fail(Wait, obtained, expected, msgs...) + } + return true + case <-time.After(timeout): + return a.failer.Fail(Wait, "timeout "+timeout.String(), "signal true", msgs...) + } +} + +// WaitClosed waits until a channel closing, the assert fails on a timeout. +func (a *Asserts) WaitClosed( + sigc <-chan interface{}, + timeout time.Duration, + msgs ...string, +) bool { + done := time.NewTimer(timeout) + defer done.Stop() + for { + select { + case _, ok := <-sigc: + if !ok { + // Only return true if channel has been closed. + return true + } + case <-done.C: + return a.failer.Fail(WaitClosed, "timeout "+timeout.String(), "closed", msgs...) + } + } +} + +// WaitGroup waits until a wait group instance is done, the assert fails on a timeout. +func (a *Asserts) WaitGroup( + wg *sync.WaitGroup, + timeout time.Duration, + msgs ...string, +) bool { + stopc := make(chan struct{}, 1) + done := time.NewTimer(timeout) + defer done.Stop() + go func() { + wg.Wait() + stopc <- struct{}{} + }() + for { + select { + case <-stopc: + return true + case <-done.C: + return a.failer.Fail(WaitGroup, "timeout "+timeout.String(), "done", msgs...) + } + } +} + +// WaitTested receives a signal from a channel and runs the passed tester +// function on it. That has to return nil for a signal assert. In case of +// a timeout the assert fails. +func (a *Asserts) WaitTested( + sigc <-chan interface{}, + tester func(interface{}) error, + timeout time.Duration, + msgs ...string, +) bool { + select { + case obtained := <-sigc: + err := tester(obtained) + return a.Nil(err, msgs...) + case <-time.After(timeout): + return a.failer.Fail(WaitTested, "timeout "+timeout.String(), "signal tested", msgs...) + } +} + +// Retry calls the passed function and expects it to return true. Otherwise +// it pauses for the given duration and retries the call the defined number. +func (a *Asserts) Retry(rf func() bool, retries int, pause time.Duration, msgs ...string) bool { + start := time.Now() + for r := 0; r < retries; r++ { + if rf() { + return true + } + time.Sleep(pause) + } + needed := time.Since(start) + info := fmt.Sprintf("timeout after %v and %d retries", needed, retries) + return a.failer.Fail(Retry, info, "successful call", msgs...) +} + +// Fail always fails. +func (a *Asserts) Fail(msgs ...string) bool { + return a.failer.Fail(Fail, nil, nil, msgs...) +} + +// MakeWaitChan is a simple one-liner to create the buffered signal channel +// for the wait assertion. +func MakeWaitChan() chan interface{} { + return make(chan interface{}, 1) +} + +// MakeMultiWaitChan is a simple one-liner to create a sized buffered signal +// channel for the wait assertion. +func MakeMultiWaitChan(size int) chan interface{} { + if size < 1 { + size = 1 + } + return make(chan interface{}, size) +} + +//-------------------- +// HELPER +//-------------------- + +// lowHigh transports the expected borders of a range test. +type lowHigh struct { + low interface{} + high interface{} +} + +// lenable is an interface for the Len() mehod. +type lenable interface { + Len() int +} + +// obexString constructs a descriptive sting matching +// to test, obtained, and expected value. +func obexString(test Test, obtained, expected interface{}) string { + switch test { + case True, False, Nil, NotNil, Empty, NotEmpty: + return fmt.Sprintf("'%v'", obtained) + case Implementor, Assignable, Unassignable: + return fmt.Sprintf("'%v' <> '%v'", ValueDescription(obtained), ValueDescription(expected)) + case Range: + lh := expected.(*lowHigh) + return fmt.Sprintf("not '%v' <= '%v' <= '%v'", lh.low, obtained, lh.high) + case Fail: + return "fail intended" + default: + return fmt.Sprintf("'%v' <> '%v'", obtained, expected) + } +} + +// failString constructs a fail string for panics or +// validition errors. +func failString(test Test, obex string, msgs ...string) string { + var out string + if test == Fail { + out = fmt.Sprintf("assert failed: %s", obex) + } else { + out = fmt.Sprintf("assert '%s' failed: %s", test, obex) + } + jmsgs := strings.Join(msgs, " ") + if len(jmsgs) > 0 { + out += " (" + jmsgs + ")" + } + return out +} + +// EOF diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go new file mode 100644 index 0000000..0c4e67a --- /dev/null +++ b/asserts/asserts_test.go @@ -0,0 +1,637 @@ +// Tideland Go Audit - Asserts - Unit Tests +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package asserts_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "tideland.dev/go/audit/asserts" +) + +//-------------------- +// TESTS +//-------------------- + +// TestAssertTrue tests the True() assertion. +func TestAssertTrue(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.True(true, "should not fail") + failingAssert.True(false, "should fail and be logged") +} + +// TestAssertFalse tests the False() assertion. +func TestAssertFalse(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.False(false, "should not fail") + failingAssert.False(true, "should fail and be logged") +} + +// TestAssertNil tests the Nil() assertion. +func TestAssertNil(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Nil(nil, "should not fail") + failingAssert.Nil("not nil", "should fail and be logged") +} + +// TestAssertNotNil tests the NotNil() assertion. +func TestAssertNotNil(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.NotNil("not nil", "should not fail") + failingAssert.NotNil(nil, "should fail and be logged") +} + +// TestAssertNoError tests the NoError() assertion. +func TestAssertNoError(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + var errA error + var errB = errors.New("ouch") + + successfulAssert.NoError(errA, "should not fail") + failingAssert.NoError(errB, "should fail and be logged") +} + +// TestAssertEqual tests the Equal() assertion. +func TestAssertEqual(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + m := map[string]int{"one": 1, "two": 2, "three": 3} + now := time.Now() + nowStr := now.Format(time.RFC3339Nano) + nowParsedA, errA := time.Parse(time.RFC3339Nano, nowStr) + nowParsedB, errB := time.Parse(time.RFC3339Nano, nowStr) + + successfulAssert.Nil(errA, "should not fail") + successfulAssert.Nil(errB, "should not fail") + successfulAssert.Equal(nowParsedA, nowParsedB, "should not fail") + successfulAssert.Equal(nil, nil, "should not fail") + successfulAssert.Equal(true, true, "should not fail") + successfulAssert.Equal(1, 1, "should not fail") + successfulAssert.Equal("foo", "foo", "should not fail") + successfulAssert.Equal(map[string]int{"one": 1, "three": 3, "two": 2}, m, "should not fail") + failingAssert.Equal("one", 1, "should fail and be logged") + failingAssert.Equal("two", "2", "should fail and be logged") +} + +// TestAssertDifferent tests the Different() assertion. +func TestAssertDifferent(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + m := map[string]int{"one": 1, "two": 2, "three": 3} + + successfulAssert.Different(nil, "nil", "should not fail") + successfulAssert.Different("true", true, "should not fail") + successfulAssert.Different(1, 2, "should not fail") + successfulAssert.Different("foo", "bar", "should not fail") + successfulAssert.Different(map[string]int{"three": 3, "two": 2}, m, "should not fail") + failingAssert.Different("one", "one", "should fail and be logged") + failingAssert.Different(2, 2, "should fail and be logged") +} + +// TestAssertAbout tests the About() assertion. +func TestAssertAbout(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.About(1.0, 1.0, 0.0, "equal, no extend") + successfulAssert.About(1.0, 1.0, 0.1, "equal, little extend") + successfulAssert.About(0.9, 1.0, 0.1, "different, within bounds of extent") + successfulAssert.About(1.1, 1.0, 0.1, "different, within bounds of extent") + failingAssert.About(0.8, 1.0, 0.1, "different, out of bounds of extent") + failingAssert.About(1.2, 1.0, 0.1, "different, out of bounds of extent") +} + +// TestAssertRange tests the Range() assertion. +func TestAssertRange(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + now := time.Now() + + successfulAssert.Range(byte(9), byte(1), byte(22), "byte in range") + successfulAssert.Range(9, 1, 22, "int in range") + successfulAssert.Range(9.0, 1.0, 22.0, "float64 in range") + successfulAssert.Range('f', 'a', 'z', "rune in range") + successfulAssert.Range("foo", "a", "zzzzz", "string in range") + successfulAssert.Range(now, now.Add(-time.Hour), now.Add(time.Hour), "time in range") + successfulAssert.Range(time.Minute, time.Second, time.Hour, "duration in range") + successfulAssert.Range([]int{1, 2, 3}, 1, 10, "slice length in range") + successfulAssert.Range([3]int{1, 2, 3}, 1, 10, "array length in range") + successfulAssert.Range(map[int]int{3: 1, 2: 2, 1: 3}, 1, 10, "map length in range") + failingAssert.Range(byte(1), byte(10), byte(20), "byte out of range") + failingAssert.Range(1, 10, 20, "int out of range") + failingAssert.Range(1.0, 10.0, 20.0, "float64 out of range") + failingAssert.Range('a', 'x', 'z', "rune out of range") + failingAssert.Range("aaa", "uuuuu", "zzzzz", "string out of range") + failingAssert.Range(now, now.Add(time.Minute), now.Add(time.Hour), "time out of range") + failingAssert.Range(time.Second, time.Minute, time.Hour, "duration in range") + failingAssert.Range([]int{1, 2, 3}, 5, 10, "slice length out of range") + failingAssert.Range([3]int{1, 2, 3}, 5, 10, "array length out of range") + failingAssert.Range(map[int]int{3: 1, 2: 2, 1: 3}, 5, 10, "map length out of range") +} + +// TestAssertContents tests the Contents() assertion. +func TestAssertContents(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Contents("bar", "foobarbaz") + successfulAssert.Contents(4711, []int{1, 2, 3, 4711, 5, 6, 7, 8, 9}) + failingAssert.Contents(4711, "12345-4711-67890") + failingAssert.Contents(4711, "foo") + failingAssert.Contents(4711, []interface{}{1, "2", 3, "4711", 5, 6, 7, 8, 9}) + successfulAssert.Contents("4711", []interface{}{1, "2", 3, "4711", 5, 6, 7, 8, 9}) + failingAssert.Contents("foobar", []byte("the quick brown fox jumps over the lazy dog")) + + successfulAssert.NotContents("yadda", "foobarbaz") + successfulAssert.NotContents(123, []int{1, 2, 3, 4711, 5, 6, 7, 8, 9}) + failingAssert.NotContents("4711", "12345-4711-67890") + failingAssert.NotContents("oba", "foobar") + failingAssert.NotContents("4711", []interface{}{1, "2", 3, "4711", 5, 6, 7, 8, 9}) + successfulAssert.NotContents(4711, []interface{}{1, "2", 3, "4711", 5, 6, 7, 8, 9}) + failingAssert.NotContents("fox", []byte("the quick brown fox jumps over the lazy dog")) +} + +// TestAssertContentsPrint test the visualization of failing content tests. +func TestAssertContentsPrint(t *testing.T) { + assert := asserts.NewTesting(t, asserts.NoFailing) + + assert.Logf("printing of failing content tests") + assert.Contents("foobar", []byte("the quick brown fox jumps over the lazy dog"), "test fails but passes, just visualization") + assert.Contents([]byte("foobar"), []byte("the quick brown ..."), "test fails but passes, just visualization") +} + +// TestOffsetPrint test the correct visualization when printing +// with offset. +func TestOffsetPrint(t *testing.T) { + assert := asserts.NewTesting(t, asserts.NoFailing) + + // Log should reference line below (174). + failWithOffset(assert, "174") +} + +// TestAssertSubstring tests the Substring() assertion. +func TestAssertSubstring(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Substring("is assert", "this is assert test", "should not fail") + successfulAssert.Substring("test", "this is 1 test", "should not fail") + failingAssert.Substring("foo", "this is assert test", "should fail and be logged") + failingAssert.Substring("this is assert test", "this is assert test", "should fail and be logged") +} + +// TestAssertCase tests the Case() assertion. +func TestAssertCase(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Case("FOO", true, "is all uppercase") + successfulAssert.Case("foo", false, "is all lowercase") + failingAssert.Case("Foo", true, "is mixed case") + failingAssert.Case("Foo", false, "is mixed case") +} + +// TestAssertMatch tests the Match() assertion. +func TestAssertMatch(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Match("this is assert test", "this.*test", "should not fail") + successfulAssert.Match("this is 1 test", "this is [0-9] test", "should not fail") + failingAssert.Match("this is assert test", "foo", "should fail and be logged") + failingAssert.Match("this is assert test", "this*test", "should fail and be logged") +} + +// TestAssertErrorMatch tests the ErrorMatch() assertion. +func TestAssertErrorMatch(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + err := errors.New("oops, an error") + + successfulAssert.ErrorMatch(err, "oops, an error", "should not fail") + successfulAssert.ErrorMatch(err, "oops,.*", "should not fail") + failingAssert.ErrorMatch(err, "foo", "should fail and be logged") +} + +// TestAssertErrorContains tests the ErrorContains() assertion. +func TestAssertErrorContains(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + err := errors.New("oops, an error") + + successfulAssert.ErrorContains(err, "an error", "should not fail") + failingAssert.ErrorContains(err, "foo", "should fail and be logged") +} + +// TestAssertImplementor tests the Implementor() assertion. +func TestAssertImplementor(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + var err error + var w io.Writer + + successfulAssert.Implementor(errors.New("error test"), &err, "should not fail") + failingAssert.Implementor("string test", &err, "should fail and be logged") + failingAssert.Implementor(errors.New("error test"), &w, "should fail and be logged") +} + +// TestAssertAssignable tests the Assignable() assertion. +func TestAssertAssignable(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Assignable(1, 5, "should not fail") + failingAssert.Assignable("one", 5, "should fail and be logged") +} + +// TestAssertUnassignable tests the Unassignable() assertion. +func TestAssertUnassignable(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Unassignable("one", 5, "should not fail") + failingAssert.Unassignable(1, 5, "should fail and be logged") +} + +// TestAssertEmpty tests the Empty() assertion. +func TestAssertEmpty(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Empty("", "should not fail") + successfulAssert.Empty([]bool{}, "should also not fail") + failingAssert.Empty("not empty", "should fail and be logged") + failingAssert.Empty([3]int{1, 2, 3}, "should also fail and be logged") + failingAssert.Empty(true, "illegal type has to fail") +} + +// TestAssertNotEmpty tests the NotEmpty() assertion. +func TestAssertNotEmpty(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.NotEmpty("not empty", "should not fail") + successfulAssert.NotEmpty([3]int{1, 2, 3}, "should also not fail") + failingAssert.NotEmpty("", "should fail and be logged") + failingAssert.NotEmpty([]int{}, "should also fail and be logged") + failingAssert.NotEmpty(true, "illegal type has to fail") +} + +// TestAssertLength tests the Length() assertion. +func TestAssertLength(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Length("", 0, "should not fail") + successfulAssert.Length([]bool{true, false}, 2, "should also not fail") + failingAssert.Length("not empty", 0, "should fail and be logged") + failingAssert.Length([3]int{1, 2, 3}, 10, "should also fail and be logged") + failingAssert.Length(true, 1, "illegal type has to fail") +} + +// TestAssertPanics tests the Panics() assertion. +func TestAssertPanics(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + successfulAssert.Panics(func() { panic("ouch") }, "should panic") + failingAssert.Panics(func() { _ = 1 + 1 }, "should not panic") +} + +// TestAssertWait tests the wait testing. +func TestAssertWait(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + waitc := asserts.MakeWaitChan() + go func() { + time.Sleep(50 * time.Millisecond) + waitc <- true + }() + successfulAssert.Wait(waitc, true, 100*time.Millisecond, "should be true") + + go func() { + time.Sleep(50 * time.Millisecond) + waitc <- false + }() + failingAssert.Wait(waitc, true, 100*time.Millisecond, "should be false") + + go func() { + time.Sleep(200 * time.Millisecond) + waitc <- true + }() + failingAssert.Wait(waitc, true, 100*time.Millisecond, "should timeout") +} + +// TestAssertWaitClosed tests the wait closed testing. +func TestAssertWaitClosed(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + waitc := asserts.MakeWaitChan() + go func() { + time.Sleep(50 * time.Millisecond) + close(waitc) + }() + successfulAssert.WaitClosed(waitc, 100*time.Millisecond, "should be true") + + waitc = asserts.MakeWaitChan() + go func() { + time.Sleep(500 * time.Millisecond) + close(waitc) + }() + failingAssert.WaitClosed(waitc, 100*time.Millisecond, "should timeout") +} + +// TestAssertWaitGroup tests the wait group testing. +func TestAssertWaitGroup(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + var wg sync.WaitGroup + + wg.Add(5) + go func() { + for i := 0; i < 5; i++ { + time.Sleep(50 * time.Millisecond) + wg.Done() + } + }() + successfulAssert.WaitGroup(&wg, 500*time.Millisecond, "should be done") + + wg.Add(5) + go func() { + for i := 0; i < 5; i++ { + time.Sleep(50 * time.Millisecond) + wg.Done() + } + }() + failingAssert.WaitGroup(&wg, 200*time.Millisecond, "should timeout") +} + +// TestAssertWaitTested tests the wait tested testing. +func TestAssertWaitTested(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + tester := func(v interface{}) error { + b, ok := v.(bool) + if !ok || b == false { + return errors.New("illegal value") + } + return nil + } + + waitc := asserts.MakeWaitChan() + go func() { + time.Sleep(50 * time.Millisecond) + waitc <- true + }() + successfulAssert.WaitTested(waitc, tester, 100*time.Millisecond, "should be true") + + go func() { + time.Sleep(50 * time.Millisecond) + waitc <- false + }() + failingAssert.WaitTested(waitc, tester, 100*time.Millisecond, "should be false") + + go func() { + time.Sleep(200 * time.Millisecond) + waitc <- true + }() + failingAssert.WaitTested(waitc, tester, 100*time.Millisecond, "should timeout") +} + +// TestAssertRetry tests the retry testing. +func TestAssertRetry(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + i := 0 + successfulAssert.Retry(func() bool { + i++ + return i == 5 + }, 10, 10*time.Millisecond, "should succeed") + + failingAssert.Retry(func() bool { return false }, 10, 10*time.Millisecond, "should fail") +} + +// TestAssertPathExists tests the PathExists() assertion. +func TestAssertPathExists(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + dir := filepath.Join(os.TempDir(), "assert-path-exists") + err := os.Mkdir(dir, 0700) + successfulAssert.Nil(err) + defer func() { + err = os.RemoveAll(dir) + successfulAssert.Nil(err) + }() + + successfulAssert.PathExists(dir, "temporary directory exists") + failingAssert.PathExists("/this/path/will/hopefully/not/exist", "illegal path") +} + +// TestAssertFail tests the fail testing. +func TestAssertFail(t *testing.T) { + failingAssert := failingAsserts(t) + + failingAssert.Fail("this should fail") +} + +// TestTestingAssertion tests the testing assertion. +func TestTestingAssertion(t *testing.T) { + assert := asserts.NewTesting(t, asserts.NoFailing) + foo := func() {} + bar := 4711 + + assert.Assignable(47, 11, "should not fail") + assert.Assignable(foo, bar, "should fail (but not the test)") + assert.Assignable(foo, bar) + assert.Assignable(foo, bar, "this", "should", "fail", "too") +} + +// TestPanicAssertion tests if the panic assertions panic when they fail. +func TestPanicAssert(t *testing.T) { + defer func() { + if err := recover(); err != nil { + t.Logf("panic worked: '%v'", err) + } + }() + + assert := asserts.NewPanic() + foo := func() {} + + assert.Assignable(47, 11, "should not fail") + assert.Assignable(47, foo, "should fail") + + t.Errorf("should not be reached") +} + +// TestValidationAssertion test the validation of data. +func TestValidationAssertion(t *testing.T) { + assert, failures := asserts.NewValidation() + + assert.True(true, "should not fail") + assert.True(false, "should fail") + assert.Equal(1, 2, "should fail") + + if !failures.HasErrors() { + t.Errorf("should have errors") + } + if len(failures.Errors()) != 2 { + t.Errorf("wrong number of errors: %v", failures.Error()) + } + + if len(failures.Details()) != 2 { + t.Errorf("wrong number of details") + } + details := failures.Details() + location, fun := details[0].Location() + tt := details[0].Test() + if location != "asserts_test.go:506:0:" || fun != "TestValidationAssertion" { + t.Errorf("wrong location %q or function %q of first detail", location, fun) + } + if tt != asserts.True { + t.Errorf("wrong test type of first detail: %v", tt) + } + location, fun = details[1].Location() + tt = details[1].Test() + if location != "asserts_test.go:507:0:" || fun != "TestValidationAssertion" { + t.Errorf("wrong location %q or function %q of second detail", location, fun) + } + if tt != asserts.Equal { + t.Errorf("wrong test type of second detail: %v", tt) + } +} + +// TestSetFailable tests the setting of the failable +// to the one of a sub-test. +func TestSetFailable(t *testing.T) { + successfulAssert := successfulAsserts(t) + failingAssert := failingAsserts(t) + + t.Run("success", func(t *testing.T) { + defer successfulAssert.SetFailable(t)() + successfulAssert.True(true) + }) + + t.Run("fail", func(t *testing.T) { + defer failingAssert.SetFailable(t)() + failingAssert.True(false) + }) +} + +// TestSetPrinter tests the chaning of the printer. +func TestSetPrinter(t *testing.T) { + assert := asserts.NewTesting(t, asserts.NoFailing) + + // Must not fail. + assert.Logf("first %d %s", 1, "(a)") + assert.Logf("second") + + // Collect in buffer. + bp := asserts.NewBufferedPrinter() + assert.SetPrinter(bp) + assert.Logf("first") + assert.Logf("second") + assert.Logf("third") + + b := bp.Flush() + assert.Length(b, 3) + assert.Contents(b[0], "first") + assert.Contents(b[1], "second") + assert.Contents(b[2], "third") + b = bp.Flush() + assert.Length(b, 0) +} + +//-------------------- +// META FAILER +//-------------------- + +type metaFailer struct { + t *testing.T + fail bool +} + +func (f *metaFailer) SetPrinter(printer asserts.Printer) asserts.Printer { + return printer +} + +func (f *metaFailer) IncrCallstackOffset() func() { + return func() {} +} + +func (f *metaFailer) Logf(format string, args ...interface{}) { + f.t.Logf(format, args...) +} + +func (f *metaFailer) Fail(test asserts.Test, obtained, expected interface{}, msgs ...string) bool { + msg := strings.Join(msgs, " ") + if msg != "" { + msg = " [" + msg + "]" + } + format := "testing assert %q failed: '%v' (%v) <> '%v' (%v)" + msg + obtainedVD := asserts.ValueDescription(obtained) + expectedVD := asserts.ValueDescription(expected) + f.Logf(format, test, obtained, obtainedVD, expected, expectedVD) + if f.fail { + f.t.FailNow() + } + return f.fail +} + +//-------------------- +// HELPER +//-------------------- + +// failWithOffset checks the offset increment. +func failWithOffset(assert *asserts.Asserts, line string) { + restore := assert.IncrCallstackOffset() + defer restore() + + assert.Fail("should fail referencing line " + line) +} + +// successfulAsserts returns an Asserts insrance which doesn't expect a failing. +func successfulAsserts(t *testing.T) *asserts.Asserts { + return asserts.New(&metaFailer{t, true}) +} + +// failingAsserts returns an Asserts instance which only logs a failing but doesn't fail. +func failingAsserts(t *testing.T) *asserts.Asserts { + return asserts.New(&metaFailer{t, false}) +} + +// EOF diff --git a/asserts/doc.go b/asserts/doc.go new file mode 100644 index 0000000..a232c3d --- /dev/null +++ b/asserts/doc.go @@ -0,0 +1,24 @@ +// Tideland Go Audit - Asserts +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +// Package asserts helps writing convenient and powerful unit tests. One part of +// those are assertions to compare expected and obtained values. Additional text output +// for failing tests can be added. +// +// In the beginning of a test function a new assertion instance is created with: +// +// assert := asserts.NewTesting(t, shallFail) +// +// Inside the test an assert looks like: +// +// assert.Equal(obtained, expected, "obtained value has to be like expected") +// +// If shallFail is set to true a failing assert also lets fail the Go test. +// Otherwise the failing is printed but the tests continue. +package asserts // import "tideland.dev/go/audit/asserts" + +// EOF diff --git a/asserts/failer.go b/asserts/failer.go new file mode 100644 index 0000000..7a83143 --- /dev/null +++ b/asserts/failer.go @@ -0,0 +1,420 @@ +// Tideland Go Audit - Asserts +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package asserts // import "tideland.dev/go/audit/asserts" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "bytes" + "errors" + "fmt" + "path" + "runtime" + "strings" + "sync" + "time" +) + +//-------------------- +// FAILER +//-------------------- + +// Failer describes a type controlling how an assert +// reacts after a failure. +type Failer interface { + // SetPrinter sets a new Printer for the Failer and + // returns the current one, e.g. for restoring. + SetPrinter(printer Printer) Printer + + // IncrCallstackOffset increases the callstack offset for + // the assertion output (see Asserts) and returns a function + // for restoring. + IncrCallstackOffset() func() + + // Logf can be used to display useful information during testing. + Logf(format string, args ...interface{}) + + // Fail will be called if an assert fails. + Fail(test Test, obtained, expected interface{}, msgs ...string) bool +} + +// FailureDetail contains detailed information of a failure. +type FailureDetail interface { + // TImestamp tells when the failure has happened. + Timestamp() time.Time + + // Locations returns file name with line number and + // function name of the failure. + Location() (string, string) + + // Test tells which kind of test has failed. + Test() Test + + // Error returns the failure as error. + Error() error + + // Message return the optional test message. + Message() string +} + +// failureDetail implements the FailureDetail interface. +type failureDetail struct { + timestamp time.Time + location string + fun string + test Test + err error + message string +} + +// TImestamp implements the FailureDetail interface. +func (d *failureDetail) Timestamp() time.Time { + return d.timestamp +} + +// Locations implements the FailureDetail interface. +func (d *failureDetail) Location() (string, string) { + return d.location, d.fun +} + +// Test implements the FailureDetail interface. +func (d *failureDetail) Test() Test { + return d.test +} + +// Error implements the FailureDetail interface. +func (d *failureDetail) Error() error { + return d.err +} + +// Message implements the FailureDetail interface. +func (d *failureDetail) Message() string { + return d.message +} + +// Failures collects the collected failures +// of a validation assertion. +type Failures interface { + // HasErrors returns true, if assertion failures happened. + HasErrors() bool + + // Details returns the collected details. + Details() []FailureDetail + + // Errors returns the so far collected errors. + Errors() []error + + // Error returns the collected errors as one error. + Error() error +} + +//-------------------- +// PANIC FAILER +//-------------------- + +// panicFailer reacts with a panic. +type panicFailer struct { + printer Printer +} + +// SetPrinter implements Failer. +func (f *panicFailer) SetPrinter(printer Printer) Printer { + old := f.printer + f.printer = printer + return old +} + +// IncrCallstackOffset implements Failer. +func (f *panicFailer) IncrCallstackOffset() func() { + return func() {} +} + +// Logf implements Failer. +func (f *panicFailer) Logf(format string, args ...interface{}) { + f.printer.Logf(format+"\n", args...) +} + +// Fail implements the Failer interface. +func (f panicFailer) Fail(test Test, obtained, expected interface{}, msgs ...string) bool { + obex := obexString(test, obtained, expected) + failStr := failString(test, obex, msgs...) + f.printer.Errorf(failStr) + panic(failStr) +} + +// NewPanic creates a new Asserts instance which panics if a test fails. +func NewPanic() *Asserts { + return New(&panicFailer{ + printer: NewStandardPrinter(), + }) +} + +//-------------------- +// VALIDATION FAILER +//-------------------- + +// validationFailer collects validation errors, e.g. when +// validating form input data. +type validationFailer struct { + mu sync.Mutex + printer Printer + offset int + details []FailureDetail + errs []error +} + +// HasErrors implements Failures. +func (f *validationFailer) HasErrors() bool { + f.mu.Lock() + defer f.mu.Unlock() + return len(f.errs) > 0 +} + +// Details implements Failures. +func (f *validationFailer) Details() []FailureDetail { + f.mu.Lock() + defer f.mu.Unlock() + return f.details +} + +// Errors implements Failures. +func (f *validationFailer) Errors() []error { + f.mu.Lock() + defer f.mu.Unlock() + return f.errs +} + +// Error implements Failures. +func (f *validationFailer) Error() error { + f.mu.Lock() + defer f.mu.Unlock() + strs := []string{} + for i, err := range f.errs { + strs = append(strs, fmt.Sprintf("[%d] %v", i, err)) + } + return errors.New(strings.Join(strs, " / ")) +} + +// SetPrinter implements Failer. +func (f *validationFailer) SetPrinter(printer Printer) Printer { + f.mu.Lock() + defer f.mu.Unlock() + old := f.printer + f.printer = printer + return old +} + +// IncrCallstackOffset implements Failer. +func (f *validationFailer) IncrCallstackOffset() func() { + f.mu.Lock() + defer f.mu.Unlock() + offset := f.offset + f.offset++ + return func() { + f.mu.Lock() + defer f.mu.Unlock() + f.offset = offset + } +} + +// Logf implements Failer. +func (f *validationFailer) Logf(format string, args ...interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + location, fun := here(f.offset) + prefix := fmt.Sprintf("%s %s(): ", location, fun) + f.printer.Logf(prefix+format+"\n", args...) +} + +// Fail implements Failer. +func (f *validationFailer) Fail(test Test, obtained, expected interface{}, msgs ...string) bool { + f.mu.Lock() + defer f.mu.Unlock() + location, fun := here(f.offset) + obex := obexString(test, obtained, expected) + err := errors.New(failString(test, obex, msgs...)) + detail := &failureDetail{ + timestamp: time.Now(), + location: location, + fun: fun, + test: test, + err: err, + message: strings.Join(msgs, " "), + } + f.details = append(f.details, detail) + f.errs = append(f.errs, err) + return false +} + +// NewValidation creates a new Asserts instance which collections +// validation failures. The returned Failures instance allows to test an access +// them. +func NewValidation() (*Asserts, Failures) { + vf := &validationFailer{ + printer: NewStandardPrinter(), + offset: 4, + details: []FailureDetail{}, + errs: []error{}, + } + return New(vf), vf +} + +//-------------------- +// TESTING FAILER +//-------------------- + +// Failable allows an assertion to signal a fail to an external instance +// like testing.T or testing.B. +type Failable interface { + Fail() + FailNow() +} + +// FailMode defines how to react on failing test asserts. +type FailMode int + +// Fail modes for test failer. +const ( + NoFailing FailMode = 0 // NoFailing simply logs a failing. + FailContinue FailMode = 1 // FailContinue logs a failing and calls Failable.Fail(). + FailStop FailMode = 2 // FailStop logs a failing and calls Failable.FailNow(). +) + +// testingFailer works together with the testing package of Go and +// may signal the fail to it. +type testingFailer struct { + mu sync.Mutex + printer Printer + failable Failable + offset int + mode FailMode +} + +// SetPrinter implements Failer. +func (f *testingFailer) SetPrinter(printer Printer) Printer { + f.mu.Lock() + defer f.mu.Unlock() + old := f.printer + f.printer = printer + return old +} + +// IncrCallstackOffset implements Failer. +func (f *testingFailer) IncrCallstackOffset() func() { + f.mu.Lock() + defer f.mu.Unlock() + offset := f.offset + f.offset++ + return func() { + f.mu.Lock() + defer f.mu.Unlock() + f.offset = offset + } +} + +// Logf implements Failer. +func (f *testingFailer) Logf(format string, args ...interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + location, fun := here(f.offset) + prefix := fmt.Sprintf("%s %s(): ", location, fun) + f.printer.Logf(prefix+format+"\n", args...) +} + +// Fail implements Failer. +func (f *testingFailer) Fail(test Test, obtained, expected interface{}, msgs ...string) bool { + f.mu.Lock() + defer f.mu.Unlock() + location, fun := here(f.offset) + buffer := &bytes.Buffer{} + + if test == Fail { + fmt.Fprintf(buffer, "%s assert in %s() failed {", location, fun) + } else { + fmt.Fprintf(buffer, "%s assert '%s' in %s() failed {", location, test, fun) + } + switch test { + case True, False, Nil, NotNil, NoError, Empty, NotEmpty, Panics: + fmt.Fprintf(buffer, "got: %v", obtained) + case Implementor, Assignable, Unassignable: + fmt.Fprintf(buffer, "got: %v, want: %v", ValueDescription(obtained), ValueDescription(expected)) + case Contents: + switch typedObtained := obtained.(type) { + case string: + fmt.Fprintf(buffer, "part: %s, full: %s", typedObtained, expected) + default: + fmt.Fprintf(buffer, "part: %v, full: %v", obtained, expected) + } + case Fail: + default: + fmt.Fprintf(buffer, "got: %v, want: %v", TypedValue(obtained), TypedValue(expected)) + } + if len(msgs) > 0 { + if buffer.Bytes()[buffer.Len()-1] != byte('{') { + fmt.Fprintf(buffer, ", ") + } + fmt.Fprintf(buffer, "info: %s", strings.Join(msgs, " ")) + } + fmt.Fprintf(buffer, "}\n") + + switch f.mode { + case NoFailing: + f.printer.Logf(buffer.String()) + case FailContinue: + f.printer.Errorf(buffer.String()) + f.failable.Fail() + case FailStop: + f.printer.Errorf(buffer.String()) + f.failable.FailNow() + } + return false +} + +// NewTesting creates a new Asserts instance for use with the testing +// package. The *testing.T has to be passed as failable, the argument. +// shallFail controls if a failing assertion also lets fail the Go test. +func NewTesting(f Failable, mode FailMode) *Asserts { + return New(&testingFailer{ + printer: NewStandardPrinter(), + failable: f, + offset: 4, + mode: mode, + }) +} + +//-------------------- +// HELPERS +//-------------------- + +// here returns the location at the given offset. +func here(offset int) (string, string) { + // Retrieve program counters. + pcs := make([]uintptr, 1) + n := runtime.Callers(offset, pcs) + if n == 0 { + return "", "" + } + pcs = pcs[:n] + // Build ID based on program counters. + frames := runtime.CallersFrames(pcs) + for { + frame, more := frames.Next() + _, fun := path.Split(frame.Function) + parts := strings.Split(fun, ".") + fun = strings.Join(parts[1:], ".") + _, file := path.Split(frame.File) + location := fmt.Sprintf("%s:%d:0:", file, frame.Line) + if !more { + return location, fun + } + } +} + +// EOF diff --git a/asserts/printer.go b/asserts/printer.go new file mode 100644 index 0000000..2049302 --- /dev/null +++ b/asserts/printer.go @@ -0,0 +1,229 @@ +// Tideland Go Audit - Asserts +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package asserts // import "tideland.dev/go/audit/asserts" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "fmt" + "os" + "reflect" +) + +//-------------------- +// TEST +//-------------------- + +// Test represents the test inside an assert. +type Test int + +// Tests provided by the assertion. +const ( + Invalid Test = iota + 1 + True + False + Nil + NotNil + NoError + Equal + Different + Contents + About + Range + Substring + Case + Match + ErrorMatch + ErrorContains + Implementor + Assignable + Unassignable + Empty + NotEmpty + Length + Panics + PathExists + Wait + WaitClosed + WaitGroup + WaitTested + Retry + Fail +) + +// testNames maps the tests to their descriptive names. +var testNames = []string{ + Invalid: "invalid", + True: "true", + False: "false", + Nil: "nil", + NotNil: "not nil", + NoError: "no error", + Equal: "equal", + Different: "different", + Contents: "contents", + About: "about", + Range: "range", + Substring: "substring", + Case: "case", + Match: "match", + ErrorMatch: "error match", + Implementor: "implementor", + Assignable: "assignable", + Unassignable: "unassignable", + Empty: "empty", + NotEmpty: "not empty", + Length: "length", + Panics: "panics", + PathExists: "path exists", + Wait: "wait", + WaitClosed: "wait closed", + WaitGroup: "wait group", + WaitTested: "wait tested", + Retry: "retry", + Fail: "fail", +} + +// String implements fmt.Stringer. +func (t Test) String() string { + if int(t) < len(testNames) { + return testNames[t] + } + return "invalid" +} + +//-------------------- +// PRINTER +//-------------------- + +// Printer allows to switch between different outputs of +// the tests. +type Printer interface { + // Logf prints a formatted logging information. + Logf(format string, args ...interface{}) + + // Errorf prints a formatted error. + Errorf(format string, args ...interface{}) +} + +// wrappedPrinter wraps a type implementing the Printer +// interface.. +type wrappedPrinter struct { + printer Printer +} + +// NewWrappedPrinter returns a printer using the passed Printer. +func NewWrappedPrinter(p Printer) Printer { + return &wrappedPrinter{ + printer: p, + } +} + +// Logf implements Printer. +func (p *wrappedPrinter) Logf(format string, args ...interface{}) { + p.printer.Logf(format, args...) +} + +// Errorf implements Printer. +func (p *wrappedPrinter) Errorf(format string, args ...interface{}) { + p.printer.Errorf(format, args...) +} + +// standardPrinter uses the standard fmt package for printing. +type standardPrinter struct{} + +// NewStandardPrinter creates a printer writing its output to +// stdout and stderr. +func NewStandardPrinter() Printer { + return &standardPrinter{} +} + +// Logf implements Printer. +func (p *standardPrinter) Logf(format string, args ...interface{}) { + fmt.Fprintf(os.Stdout, format, args...) +} + +// Errorf implements Printer. +func (p *standardPrinter) Errorf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) +} + +// BufferedPrinter collects prints to be retrieved later via Flush(). +type BufferedPrinter interface { + Printer + + // Flush returns and resets the buffered prints. + Flush() []string +} + +// bufferedPrinter collects the prints which can be retrieved later. +type bufferedPrinter struct { + buffer []string +} + +// NewBufferedPrinter returns the buffered printer for collecting +// assertion output. +func NewBufferedPrinter() BufferedPrinter { + return &bufferedPrinter{} +} + +// Logf implements Printer. +func (p *bufferedPrinter) Logf(format string, args ...interface{}) { + s := fmt.Sprintf("[LOG] "+format, args...) + p.buffer = append(p.buffer, s) +} + +// Errorf implements Printer. +func (p *bufferedPrinter) Errorf(format string, args ...interface{}) { + s := fmt.Sprintf("[ERR] "+format, args...) + p.buffer = append(p.buffer, s) +} + +// Flush implements BufferedPrinter. +func (p *bufferedPrinter) Flush() []string { + b := p.buffer + p.buffer = nil + return b +} + +//-------------------- +// HELPER +//-------------------- + +// ValueDescription returns a description of a value as string. +func ValueDescription(value interface{}) string { + rvalue := reflect.ValueOf(value) + kind := rvalue.Kind() + switch kind { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return kind.String() + " of " + rvalue.Type().Elem().String() + case reflect.Func: + return kind.String() + " " + rvalue.Type().Name() + "()" + case reflect.Interface, reflect.Struct: + return kind.String() + " " + rvalue.Type().Name() + case reflect.Ptr: + return kind.String() + " to " + rvalue.Type().Elem().String() + default: + return kind.String() + } +} + +// TypedValue returns a value including its type. +func TypedValue(value interface{}) string { + kind := reflect.ValueOf(value).Kind() + switch kind { + case reflect.String: + return fmt.Sprintf("%q (string)", value) + default: + return fmt.Sprintf("%v (%s)", value, kind.String()) + } +} + +// EOF diff --git a/asserts/tester.go b/asserts/tester.go new file mode 100644 index 0000000..b462b9c --- /dev/null +++ b/asserts/tester.go @@ -0,0 +1,261 @@ +// Tideland Go Libray - Audit - Asserts +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package asserts // import "tideland.dev/go/audit/asserts" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "bytes" + "errors" + "fmt" + "os" + "reflect" + "regexp" + "strings" + "time" +) + +//-------------------- +// TESTER +//-------------------- + +// Tester is a helper which can be used in own Assertion implementations. +type Tester struct{} + +// IsTrue checks if obtained is true. +func (t Tester) IsTrue(obtained bool) bool { + return obtained +} + +// IsNil checks if obtained is nil in a safe way. +func (t Tester) IsNil(obtained interface{}) bool { + if obtained == nil { + // Standard test. + return true + } + // Some types have to be tested via reflection. + value := reflect.ValueOf(obtained) + kind := value.Kind() + switch kind { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return value.IsNil() + } + return false +} + +// IsEqual checks if obtained and expected are equal. +func (t Tester) IsEqual(obtained, expected interface{}) bool { + return reflect.DeepEqual(obtained, expected) +} + +// IsAbout checks if obtained and expected are to a given extent almost equal. +func (t Tester) IsAbout(obtained, expected, extent float64) bool { + if extent < 0.0 { + extent = extent * (-1) + } + low := expected - extent + high := expected + extent + return low <= obtained && obtained <= high +} + +// IsInRange checks for range assertions +func (t Tester) IsInRange(obtained, low, high interface{}) (bool, error) { + // First standard types. + switch o := obtained.(type) { + case byte: + l, lok := low.(byte) + h, hok := high.(byte) + if !lok && !hok { + return false, errors.New("low and/or high are no byte") + } + return l <= o && o <= h, nil + case int: + l, lok := low.(int) + h, hok := high.(int) + if !lok && !hok { + return false, errors.New("low and/or high are no int") + } + return l <= o && o <= h, nil + case float64: + l, lok := low.(float64) + h, hok := high.(float64) + if !lok && !hok { + return false, errors.New("low and/or high are no float64") + } + return l <= o && o <= h, nil + case rune: + l, lok := low.(rune) + h, hok := high.(rune) + if !lok && !hok { + return false, errors.New("low and/or high are no rune") + } + return l <= o && o <= h, nil + case string: + l, lok := low.(string) + h, hok := high.(string) + if !lok && !hok { + return false, errors.New("low and/or high are no string") + } + return l <= o && o <= h, nil + case time.Time: + l, lok := low.(time.Time) + h, hok := high.(time.Time) + if !lok && !hok { + return false, errors.New("low and/or high are no time") + } + return (l.Equal(o) || l.Before(o)) && + (h.After(o) || h.Equal(o)), nil + case time.Duration: + l, lok := low.(time.Duration) + h, hok := high.(time.Duration) + if !lok && !hok { + return false, errors.New("low and/or high are no duration") + } + return l <= o && o <= h, nil + } + // Now check the collection types. + ol, err := t.Len(obtained) + if err != nil { + return false, errors.New("no valid type with a length") + } + l, lok := low.(int) + h, hok := high.(int) + if !lok && !hok { + return false, errors.New("low and/or high are no int") + } + return l <= ol && ol <= h, nil +} + +// Contains checks if the part type is matching to the full type and +// if the full data contains the part data. +func (t Tester) Contains(part, full interface{}) (bool, error) { + switch fullValue := full.(type) { + case string: + // Content of a string. + switch partValue := part.(type) { + case string: + return strings.Contains(fullValue, partValue), nil + case []byte: + return strings.Contains(fullValue, string(partValue)), nil + default: + partString := fmt.Sprintf("%v", partValue) + return strings.Contains(fullValue, partString), nil + } + case []byte: + // Content of a byte slice. + switch partValue := part.(type) { + case string: + return bytes.Contains(fullValue, []byte(partValue)), nil + case []byte: + return bytes.Contains(fullValue, partValue), nil + default: + partBytes := []byte(fmt.Sprintf("%v", partValue)) + return bytes.Contains(fullValue, partBytes), nil + } + default: + // Content of any array or slice, use reflection. + value := reflect.ValueOf(full) + kind := value.Kind() + if kind == reflect.Array || kind == reflect.Slice { + length := value.Len() + for i := 0; i < length; i++ { + current := value.Index(i) + if reflect.DeepEqual(part, current.Interface()) { + return true, nil + } + } + return false, nil + } + } + return false, errors.New("full value is no string, array, or slice") +} + +// IsSubstring checks if obtained is a substring of the full string. +func (t Tester) IsSubstring(obtained, full string) bool { + return strings.Contains(full, obtained) +} + +// IsCase checks if the obtained string is uppercase or lowercase. +func (t Tester) IsCase(obtained string, upperCase bool) bool { + if upperCase { + return obtained == strings.ToUpper(obtained) + } + return obtained == strings.ToLower(obtained) +} + +// IsMatching checks if the obtained string matches a regular expression. +func (t Tester) IsMatching(obtained, regex string) (bool, error) { + return regexp.MatchString("^"+regex+"$", obtained) +} + +// IsImplementor checks if obtained implements the expected interface variable pointer. +func (t Tester) IsImplementor(obtained, expected interface{}) (bool, error) { + obtainedValue := reflect.ValueOf(obtained) + expectedValue := reflect.ValueOf(expected) + if !obtainedValue.IsValid() { + return false, fmt.Errorf("obtained value is invalid: %v", obtained) + } + if !expectedValue.IsValid() || expectedValue.Kind() != reflect.Ptr || expectedValue.Elem().Kind() != reflect.Interface { + return false, fmt.Errorf("expected value is no interface variable pointer: %v", expected) + } + return obtainedValue.Type().Implements(expectedValue.Elem().Type()), nil +} + +// IsAssignable checks if the types of obtained and expected are assignable. +func (t Tester) IsAssignable(obtained, expected interface{}) bool { + obtainedValue := reflect.ValueOf(obtained) + expectedValue := reflect.ValueOf(expected) + return obtainedValue.Type().AssignableTo(expectedValue.Type()) +} + +// Len checks the length of the obtained string, array, slice, map or channel. +func (t Tester) Len(obtained interface{}) (int, error) { + // Check using the lenable interface. + if l, ok := obtained.(lenable); ok { + return l.Len(), nil + } + // Check the standard types. + obtainedValue := reflect.ValueOf(obtained) + obtainedKind := obtainedValue.Kind() + switch obtainedKind { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return obtainedValue.Len(), nil + default: + descr := ValueDescription(obtained) + return 0, fmt.Errorf("obtained %s is no array, chan, map, slice, string or understands Len()", descr) + } +} + +// HasPanic checks if the passed function panics. +func (t Tester) HasPanic(pf func()) (ok bool) { + defer func() { + if r := recover(); r != nil { + // Panic, that's ok! + ok = true + } + }() + pf() + return false +} + +// IsValidPath checks if the given directory or +// file path exists. +func (t Tester) IsValidPath(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +// EOF diff --git a/capture/capture.go b/capture/capture.go new file mode 100644 index 0000000..35f7302 --- /dev/null +++ b/capture/capture.go @@ -0,0 +1,112 @@ +// Tideland Go Audit - Capture +// +// Copyright (C) 2017-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package capture // import "tideland.dev/go/audit/capture" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "bytes" + "io" + "log" + "os" +) + +//-------------------- +// CAPTURED +//-------------------- + +// Captured provides access to the captured output in +// multiple ways. +type Captured struct { + buffer []byte +} + +// Bytes returns the captured content as bytes. +func (c Captured) Bytes() []byte { + buf := make([]byte, c.Len()) + copy(buf, c.buffer) + return buf +} + +// String implements fmt.Stringer. +func (c Captured) String() string { + return string(c.Bytes()) +} + +// Len returns the number of captured bytes. +func (c Captured) Len() int { + return len(c.buffer) +} + +//-------------------- +// CAPTURING +//-------------------- + +// Stdout captures Stdout. +func Stdout(f func()) Captured { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + f() + + outC := make(chan []byte) + + go func() { + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + log.Fatalf("error capturing stdout: %v", err) + } + outC <- buf.Bytes() + }() + + w.Close() + os.Stdout = old + return Captured{ + buffer: <-outC, + } +} + +// Stderr captures Stderr. +func Stderr(f func()) Captured { + old := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + f() + + outC := make(chan []byte) + + go func() { + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + log.Fatalf("error capturing stderr: %v", err) + } + outC <- buf.Bytes() + }() + + w.Close() + os.Stderr = old + return Captured{ + buffer: <-outC, + } +} + +// Both captures Stdout and Stderr. +func Both(f func()) (Captured, Captured) { + var cerr Captured + ff := func() { + cerr = Stderr(f) + } + cout := Stdout(ff) + return cout, cerr +} + +// EOF diff --git a/capture/capture_test.go b/capture/capture_test.go new file mode 100644 index 0000000..b641bad --- /dev/null +++ b/capture/capture_test.go @@ -0,0 +1,97 @@ +// Tideland Go Audit - Capture - Unit Tests +// +// Copyright (C) 2017-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package capture_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "fmt" + "os" + "testing" + + "tideland.dev/go/audit/asserts" + "tideland.dev/go/audit/capture" +) + +//-------------------- +// TESTS +//-------------------- + +// TestStdout tests the capturing of writings to stdout. +func TestStdout(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + hello := "Hello, World!" + cptrd := capture.Stdout(func() { + fmt.Print(hello) + }) + assert.Equal(cptrd.String(), hello) + assert.Length(cptrd, len(hello)) +} + +// TestStderr tests the capturing of writings to stderr. +func TestStderr(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + ouch := "ouch" + cptrd := capture.Stderr(func() { + fmt.Fprint(os.Stderr, ouch) + }) + assert.Equal(cptrd.String(), ouch) + assert.Length(cptrd, len(ouch)) +} + +// TestBoth tests the capturing of writings to stdout +// and stderr. +func TestBoth(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + hello := "Hello, World!" + ouch := "ouch" + cout, cerr := capture.Both(func() { + fmt.Fprint(os.Stdout, hello) + fmt.Fprint(os.Stderr, ouch) + }) + assert.Equal(cout.String(), hello) + assert.Length(cout, len(hello)) + assert.Equal(cerr.String(), ouch) + assert.Length(cerr, len(ouch)) +} + +// TestBytes tests the retrieving of captures as bytes. +func TestBytes(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + foo := "foo" + boo := []byte(foo) + cout, cerr := capture.Both(func() { + fmt.Fprint(os.Stdout, foo) + fmt.Fprint(os.Stderr, foo) + }) + assert.Equal(cout.Bytes(), boo) + assert.Equal(cerr.Bytes(), boo) +} + +// TestRestore tests the restoring of os.Stdout +// and os.Stderr after capturing. +func TestRestore(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + foo := "foo" + oldOut := os.Stdout + oldErr := os.Stderr + cout, cerr := capture.Both(func() { + fmt.Fprint(os.Stdout, foo) + fmt.Fprint(os.Stderr, foo) + }) + assert.Equal(cout.String(), foo) + assert.Length(cout, len(foo)) + assert.Equal(cerr.String(), foo) + assert.Length(cerr, len(foo)) + assert.Equal(os.Stdout, oldOut) + assert.Equal(os.Stderr, oldErr) +} + +// EOF diff --git a/capture/doc.go b/capture/doc.go new file mode 100644 index 0000000..aea3a2b --- /dev/null +++ b/capture/doc.go @@ -0,0 +1,24 @@ +// Tideland Go Audit - Capture +// +// Copyright (C) 2017-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +// Package capture assists in testing code writing output to stdout or stderr. +// Those will be temporarily exchanged so that the written output will be +// caught and can be retrieved. +// +// cout := capture.Stdout(func() { +// fmt.Printf("Hello, World!") +// }) +// cerr := capture.Stderr(func() { ... }) +// +// assert.Equal(cout.String(), "Hello, World!") +// +// cout, cerr = capture.Both(func() { ... }) +// +// The captured content data also can be retrieved as bytes. +package capture // import "tideland.dev/go/audit/capture" + +// EOF diff --git a/environments/doc.go b/environments/doc.go new file mode 100644 index 0000000..665030d --- /dev/null +++ b/environments/doc.go @@ -0,0 +1,18 @@ +// Tideland Go Audit - Environments +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +// Package environments helps providing environments for unit tests. Here +// you can manipulate environment variables or create temporary directories +// to be used in tests and cleared afterwards. +// +// For web applications there's the web asserter allowing to (a) run tests +// against own http.Handler and http.HandlerFunc as well as (b) act as a +// mock for own clients using HTTP. Included request and response types +// simplify most usual access. +package environments // import "tideland.dev/go/audit/environments" + +// EOF diff --git a/environments/environments.go b/environments/environments.go new file mode 100644 index 0000000..6e63eb9 --- /dev/null +++ b/environments/environments.go @@ -0,0 +1,162 @@ +// Tideland Go Audit - Environments +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package environments // import "tideland.dev/go/audit/environments" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "crypto/rand" + "fmt" + "os" + "path/filepath" + + "tideland.dev/go/audit/asserts" +) + +//-------------------- +// TEMPDIR +//-------------------- + +// TempDir represents a temporary directory and possible subdirectories +// for testing purposes. It simply is created with +// +// assert := asserts.NewTesting(t, asserts.FailContinue) +// td := environments.NewTempDir(assert) +// defer td.Restore() +// +// tdName := td.String() +// subName:= td.Mkdir("my", "sub", "directory") +// +// The deferred Restore() removes the temporary directory with all +// contents. +type TempDir struct { + assert *asserts.Asserts + dir string +} + +// NewTempDir creates a new temporary directory usable for direct +// usage or further subdirectories. +func NewTempDir(assert *asserts.Asserts) *TempDir { + id := make([]byte, 8) + td := &TempDir{ + assert: assert, + } + for i := 0; i < 256; i++ { + _, err := rand.Read(id[:]) + td.assert.Nil(err) + dir := filepath.Join(os.TempDir(), fmt.Sprintf("goaudit-%x", id)) + if err = os.Mkdir(dir, 0700); err == nil { + td.dir = dir + break + } + if td.dir == "" { + msg := fmt.Sprintf("cannot create temporary directory %q: %v", td.dir, err) + td.assert.Fail(msg) + return nil + } + } + return td +} + +// Restore deletes the temporary directory and all contents. +func (td *TempDir) Restore() { + err := os.RemoveAll(td.dir) + if err != nil { + msg := fmt.Sprintf("cannot remove temporary directory %q: %v", td.dir, err) + td.assert.Fail(msg) + } +} + +// Mkdir creates a potentially nested directory inside the +// temporary directory. +func (td *TempDir) Mkdir(name ...string) string { + innerName := filepath.Join(name...) + fullName := filepath.Join(td.dir, innerName) + if err := os.MkdirAll(fullName, 0700); err != nil { + msg := fmt.Sprintf("cannot create nested temporary directory %q: %v", fullName, err) + td.assert.Fail(msg) + } + return fullName +} + +// String returns the temporary directory. +func (td *TempDir) String() string { + return td.dir +} + +//-------------------- +// VARIABLES +//-------------------- + +// Variables allows to change and restore environment variables. The +// same variable can be set multiple times. Simply do +// +// assert := asserts.NewTesting(t, asserts.FailContinue) +// ev := environments.NewVariables(assert) +// defer ev.Restore() +// +// ev.Set("MY_VAR", myValue) +// +// ... +// +// ev.Set("MY_VAR", anotherValue) +// +// The deferred Restore() resets to the original values. +type Variables struct { + assert *asserts.Asserts + vars map[string]string +} + +// NewVariables create a new changer for environment variables. +func NewVariables(assert *asserts.Asserts) *Variables { + v := &Variables{ + assert: assert, + vars: make(map[string]string), + } + return v +} + +// Restore resets all changed environment variables +func (v *Variables) Restore() { + for key, value := range v.vars { + if err := os.Setenv(key, value); err != nil { + msg := fmt.Sprintf("cannot reset environment variable %q: %v", key, err) + v.assert.Fail(msg) + } + } +} + +// Set sets an environment variable to a new value. +func (v *Variables) Set(key, value string) { + ov := os.Getenv(key) + _, ok := v.vars[key] + if !ok { + v.vars[key] = ov + } + if err := os.Setenv(key, value); err != nil { + msg := fmt.Sprintf("cannot set environment variable %q: %v", key, err) + v.assert.Fail(msg) + } +} + +// Unset unsets an environment variable. +func (v *Variables) Unset(key string) { + ov := os.Getenv(key) + _, ok := v.vars[key] + if !ok { + v.vars[key] = ov + } + if err := os.Unsetenv(key); err != nil { + msg := fmt.Sprintf("cannot unset environment variable %q: %v", key, err) + v.assert.Fail(msg) + } +} + +// EOF diff --git a/environments/environments_test.go b/environments/environments_test.go new file mode 100644 index 0000000..34c8c49 --- /dev/null +++ b/environments/environments_test.go @@ -0,0 +1,115 @@ +// Tideland Go Audit - Environments - Unit Tests +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package environments_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "os" + "testing" + + "tideland.dev/go/audit/asserts" + "tideland.dev/go/audit/environments" +) + +//-------------------- +// TESTS +//-------------------- + +// TestTempDirCreate tests the creation of temporary directories. +func TestTempDirCreate(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + testDir := func(dir string) { + fi, err := os.Stat(dir) + assert.Nil(err) + assert.True(fi.IsDir()) + assert.Equal(fi.Mode().Perm(), os.FileMode(0700)) + } + + td := environments.NewTempDir(assert) + assert.NotNil(td) + defer td.Restore() + + tds := td.String() + assert.NotEmpty(tds) + testDir(tds) + + sda := td.Mkdir("subdir", "foo") + assert.NotEmpty(sda) + testDir(sda) + sdb := td.Mkdir("subdir", "bar") + assert.NotEmpty(sdb) + testDir(sdb) +} + +// TestTempDirRestore tests the restoring of temporary created +// directories. +func TestTempDirRestore(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + + td := environments.NewTempDir(assert) + assert.NotNil(td) + tds := td.String() + fi, err := os.Stat(tds) + assert.Nil(err) + assert.True(fi.IsDir()) + + td.Restore() + _, err = os.Stat(tds) + assert.ErrorMatch(err, "stat .* no such file or directory") +} + +// TestEnvVarsSet tests the setting of temporary environment variables. +func TestEnvVarsSet(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + testEnv := func(key, value string) { + v := os.Getenv(key) + assert.Equal(v, value) + } + + ev := environments.NewVariables(assert) + assert.NotNil(ev) + defer ev.Restore() + + ev.Set("TESTING_ENV_A", "FOO") + testEnv("TESTING_ENV_A", "FOO") + ev.Set("TESTING_ENV_B", "BAR") + testEnv("TESTING_ENV_B", "BAR") + + ev.Unset("TESTING_ENV_A") + testEnv("TESTING_ENV_A", "") +} + +// TestEnvVarsREstore tests the restoring of temporary set environment +// variables. +func TestEnvVarsRestore(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + testEnv := func(key, value string) { + v := os.Getenv(key) + assert.Equal(v, value) + } + + ev := environments.NewVariables(assert) + assert.NotNil(ev) + + path := os.Getenv("PATH") + assert.NotEmpty(path) + + ev.Set("PATH", "/foo:/bar/bin") + testEnv("PATH", "/foo:/bar/bin") + ev.Set("PATH", "/bar:/foo:/yadda/bin") + testEnv("PATH", "/bar:/foo:/yadda/bin") + + ev.Restore() + + testEnv("PATH", path) +} + +// EOF diff --git a/environments/web.go b/environments/web.go new file mode 100644 index 0000000..29131e8 --- /dev/null +++ b/environments/web.go @@ -0,0 +1,421 @@ +// Tideland Go Audit - Environments +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package environments // import "tideland.dev/go/audit/environments" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "html/template" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + "regexp" + + "tideland.dev/go/audit/asserts" +) + +//-------------------- +// CONSTANTS +//-------------------- + +// Header and content-types. +const ( + HeaderAccept = "Accept" + HeaderContentType = "Content-Type" + + ContentTypePlain = "text/plain" + ContentTypeHTML = "text/html" + ContentTypeXML = "application/xml" + ContentTypeJSON = "application/json" + ContentTypeURLEncoded = "application/x-www-form-urlencoded" +) + +//-------------------- +// VALUES +//-------------------- + +// Values wraps header, cookie, query, and form values. +type Values struct { + wa *WebAsserter + data map[string][]string +} + +// newValues creates a new values instance. +func newValues(wa *WebAsserter) *Values { + vs := &Values{ + wa: wa, + data: make(map[string][]string), + } + return vs +} + +// consumeHeader consumes its values from the HTTP response header. +func consumeHeader(wa *WebAsserter, resp *http.Response) *Values { + vs := newValues(wa) + for key, values := range resp.Header { + for _, value := range values { + vs.Add(key, value) + } + } + return vs +} + +// consumeCookies consumes its values from the HTTP response cookies. +func consumeCookies(wa *WebAsserter, resp *http.Response) *Values { + vs := newValues(wa) + for _, cookie := range resp.Cookies() { + vs.Add(cookie.Name, cookie.Value) + } + return vs +} + +// Add adds or appends a value to a named field. +func (vs *Values) Add(key, value string) { + kd := append(vs.data[key], value) + vs.data[key] = kd +} + +// Set sets value of a named field. +func (vs *Values) Set(key, value string) { + vs.data[key] = []string{value} +} + +// Get returns the values for the passed key. May be nil. +func (vs *Values) Get(key string) []string { + return vs.data[key] +} + +// AssertKeyExists tests if the values contain the passed key. +func (vs *Values) AssertKeyExists(key string, msgs ...string) { + restore := vs.wa.assert.IncrCallstackOffset() + defer restore() + _, ok := vs.data[key] + vs.wa.assert.True(ok, msgs...) +} + +// AssertKeyContainsValue tests if the values contain the passed key +// and that the passed value. +func (vs *Values) AssertKeyContainsValue(key, expected string, msgs ...string) { + restore := vs.wa.assert.IncrCallstackOffset() + defer restore() + kd, ok := vs.data[key] + vs.wa.assert.True(ok, msgs...) + vs.wa.assert.Contents(expected, kd, msgs...) +} + +// AssertKeyValueEquals tests if the first value for a key equals the expected value. +func (vs *Values) AssertKeyValueEquals(key, expected string, msgs ...string) { + restore := vs.wa.assert.IncrCallstackOffset() + defer restore() + values, ok := vs.data[key] + vs.wa.assert.True(ok, msgs...) + vs.wa.assert.NotEmpty(values, msgs...) + vs.wa.assert.Equal(values[0], expected, msgs...) +} + +// applyHeader applies its values to the HTTP request header. +func (vs *Values) applyHeader(r *http.Request) { + for key, values := range vs.data { + for _, value := range values { + r.Header.Add(key, value) + } + } +} + +// applyCookies applies its values to the HTTP request cookies. +func (vs *Values) applyCookies(r *http.Request) { + restore := vs.wa.assert.IncrCallstackOffset() + defer restore() + for key, kd := range vs.data { + vs.wa.assert.NotEmpty(kd, "cookie must not be empty") + cookie := &http.Cookie{ + Name: key, + Value: kd[0], + } + r.AddCookie(cookie) + } +} + +//-------------------- +// WEB RESPONSE +//-------------------- + +// WebResponse provides simplified access to a response in context of +// a web asserter. +type WebResponse struct { + wa *WebAsserter + resp *http.Response + header *Values + cookies *Values + body []byte +} + +// Header returns the header values of the response. +func (wresp *WebResponse) Header() *Values { + return wresp.header +} + +// Cookies returns the cookie values of the response. +func (wresp *WebResponse) Cookies() *Values { + return wresp.cookies +} + +// Body returns the body of the response. +func (wresp *WebResponse) Body() []byte { + return wresp.body +} + +// AssertStatusCodeEquals checks if the status is the expected one. +func (wresp *WebResponse) AssertStatusCodeEquals(expected int) { + restore := wresp.wa.assert.IncrCallstackOffset() + defer restore() + wresp.wa.assert.Equal(wresp.resp.StatusCode, expected, "response status differs") +} + +// AssertUnmarshalledBody retrieves the body based on the content type +// and unmarshals it accordingly. It asserts that everything works fine. +func (wresp *WebResponse) AssertUnmarshalledBody(data interface{}) { + restore := wresp.wa.assert.IncrCallstackOffset() + defer restore() + contentType := wresp.header.Get(HeaderContentType) + wresp.wa.assert.NotEmpty(contentType) + switch contentType[0] { + case ContentTypeJSON: + err := json.Unmarshal(wresp.body, data) + wresp.wa.assert.Nil(err, "cannot unmarshal JSON body") + case ContentTypeXML: + err := xml.Unmarshal(wresp.body, data) + wresp.wa.assert.Nil(err, "cannot unmarshal XML body") + default: + wresp.wa.assert.Fail("unmarshalled content type: " + contentType[0]) + } +} + +// AssertBodyMatches checks if the body matches a regular expression. +func (wresp *WebResponse) AssertBodyMatches(pattern string) { + restore := wresp.wa.assert.IncrCallstackOffset() + defer restore() + ok, err := regexp.MatchString(pattern, string(wresp.body)) + wresp.wa.assert.Nil(err, "illegal content match pattern") + wresp.wa.assert.True(ok, "body doesn't match pattern") +} + +// AssertBodyGrep greps content out of the body. +func (wresp *WebResponse) AssertBodyGrep(pattern string) []string { + restore := wresp.wa.assert.IncrCallstackOffset() + defer restore() + expr, err := regexp.Compile(pattern) + wresp.wa.assert.Nil(err, "illegal content grep pattern") + return expr.FindAllString(string(wresp.body), -1) +} + +// AssertBodyContains checks if the body contains a string. +func (wresp *WebResponse) AssertBodyContains(expected string) { + restore := wresp.wa.assert.IncrCallstackOffset() + defer restore() + wresp.wa.assert.Contents(expected, wresp.body, "body doesn't contains expected") +} + +//-------------------- +// WEB REQUEST +//-------------------- + +// WebRequest provides simplified access to a request in context of +// a web asserter. +type WebRequest struct { + wa *WebAsserter + method string + path string + header *Values + cookies *Values + fieldname string + filename string + body []byte +} + +// Header returns a values instance for request header. +func (wreq *WebRequest) Header() *Values { + if wreq.header == nil { + wreq.header = newValues(wreq.wa) + } + return wreq.header +} + +// Cookies returns a values instance for request cookies. +func (wreq *WebRequest) Cookies() *Values { + if wreq.cookies == nil { + wreq.cookies = newValues(wreq.wa) + } + return wreq.cookies +} + +// SetContentType sets the header Content-Type. +func (wreq *WebRequest) SetContentType(contentType string) { + wreq.Header().Add(HeaderContentType, contentType) +} + +// SetAccept sets the header Accept. +func (wreq *WebRequest) SetAccept(contentType string) { + wreq.Header().Set(HeaderAccept, contentType) +} + +// Upload sets the request as a file upload request. +func (wreq *WebRequest) Upload(fieldname, filename, data string) { + wreq.fieldname = fieldname + wreq.filename = filename + wreq.body = []byte(data) +} + +// AssertMarshalBody sets the request body based on the set content type and +// the marshalled data and asserts that everything works fine. +func (wreq *WebRequest) AssertMarshalBody(data interface{}) { + restore := wreq.wa.assert.IncrCallstackOffset() + defer restore() + // Marshal the passed data into the request body. + contentType := wreq.Header().Get(HeaderContentType) + wreq.wa.assert.NotEmpty(contentType, "content type must be set for marshalling") + switch contentType[0] { + case ContentTypeJSON: + body, err := json.Marshal(data) + wreq.wa.assert.Nil(err, "cannot marshal data to JSON") + wreq.body = body + wreq.Header().Add(HeaderContentType, ContentTypeJSON) + wreq.Header().Add(HeaderAccept, ContentTypeJSON) + case ContentTypeXML: + body, err := xml.Marshal(data) + wreq.wa.assert.Nil(err, "cannot marshal data to XML") + wreq.body = body + wreq.Header().Add(HeaderContentType, ContentTypeXML) + wreq.Header().Add(HeaderAccept, ContentTypeXML) + } +} + +// AssertRenderTemplate renders the passed data into the template and +// assigns it to the request body. It asserts that everything works fine. +func (wreq *WebRequest) AssertRenderTemplate(templateSource string, data interface{}) { + restore := wreq.wa.assert.IncrCallstackOffset() + defer restore() + // Render template. + t, err := template.New(wreq.path).Parse(templateSource) + wreq.wa.assert.Nil(err, "cannot parse template") + body := &bytes.Buffer{} + err = t.Execute(body, data) + wreq.wa.assert.Nil(err, "cannot render template") + wreq.body = body.Bytes() +} + +// Do performes the web request with the passed method. +func (wreq *WebRequest) Do() *WebResponse { + restore := wreq.wa.assert.IncrCallstackOffset() + defer restore() + // First prepare it. + var bodyReader io.Reader + if wreq.filename != "" { + // Upload file content. + buffer := &bytes.Buffer{} + writer := multipart.NewWriter(buffer) + part, err := writer.CreateFormFile(wreq.fieldname, wreq.filename) + wreq.wa.assert.Nil(err, "cannot create form file") + _, err = io.WriteString(part, string(wreq.body)) + wreq.wa.assert.Nil(err, "cannot write data") + wreq.SetContentType(writer.FormDataContentType()) + err = writer.Close() + wreq.wa.assert.Nil(err, "cannot close multipart writer") + wreq.method = http.MethodPost + bodyReader = ioutil.NopCloser(buffer) + } else if wreq.body != nil { + // Upload body content. + bodyReader = ioutil.NopCloser(bytes.NewBuffer(wreq.body)) + } + req, err := http.NewRequest(wreq.method, wreq.wa.URL()+wreq.path, bodyReader) + wreq.wa.assert.Nil(err, "cannot prepare request") + wreq.Header().applyHeader(req) + wreq.Cookies().applyCookies(req) + // Create client and perform request. + c := http.Client{ + Transport: &http.Transport{}, + } + resp, err := c.Do(req) + wreq.wa.assert.Nil(err, "cannot perform test request") + // Create web response. + wresp := &WebResponse{ + wa: wreq.wa, + resp: resp, + header: consumeHeader(wreq.wa, resp), + cookies: consumeCookies(wreq.wa, resp), + } + body, err := ioutil.ReadAll(resp.Body) + wreq.wa.assert.Nil(err, "cannot read response") + defer resp.Body.Close() + wresp.body = body + return wresp +} + +//-------------------- +// WEB ASSERTER +//-------------------- + +// WebAsserter defines the test server with methods for requests +// and uploads. +type WebAsserter struct { + assert *asserts.Asserts + server *httptest.Server + mux *http.ServeMux +} + +// NewWebAsserter creates a web test server for the tests of own handler +// or the mocking of external systems. +func NewWebAsserter(assert *asserts.Asserts) *WebAsserter { + wa := &WebAsserter{ + assert: assert, + mux: http.NewServeMux(), + } + wa.server = httptest.NewServer(wa.mux) + return wa +} + +// Handle registers the handler for the given pattern. If a handler +// already exists for pattern, Handle panics. +func (wa *WebAsserter) Handle(pattern string, handler http.Handler) { + wa.mux.Handle(pattern, handler) +} + +// HandleFunc registers the handler function for the given pattern +func (wa *WebAsserter) HandleFunc(pattern string, handler func(w http.ResponseWriter, r *http.Request)) { + wa.mux.HandleFunc(pattern, handler) +} + +// URL returns the local URL of the internal test server. +func (wa *WebAsserter) URL() string { + return wa.server.URL +} + +// Close shuts down the internal test server and blocks until all +// outstanding requests have completed. +func (wa *WebAsserter) Close() { + wa.server.Close() +} + +// CreateRequest prepares a web request to be performed +// against this web asserter. +func (wa *WebAsserter) CreateRequest(method, path string) *WebRequest { + return &WebRequest{ + wa: wa, + method: method, + path: path, + } +} + +// EOF diff --git a/environments/web_test.go b/environments/web_test.go new file mode 100644 index 0000000..de320c9 --- /dev/null +++ b/environments/web_test.go @@ -0,0 +1,154 @@ +// Tideland Go Audit - Environments - Unit Tests +// +// Copyright (C) 2012-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package environments_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "net/http" + "testing" + + "tideland.dev/go/audit/asserts" + "tideland.dev/go/audit/environments" +) + +//-------------------- +// TESTS +//-------------------- + +// TestSimpleRequests tests simple requests to individual handlers. +func TestSimpleRequests(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + wa := StartWebAsserter(assert) + defer wa.Close() + + tests := []struct { + method string + path string + statusCode int + contentType string + body string + }{ + { + method: http.MethodGet, + path: "/hello/world", + statusCode: http.StatusOK, + contentType: environments.ContentTypePlain, + body: "Hello, World!", + }, { + method: http.MethodGet, + path: "/hello/tester", + statusCode: http.StatusOK, + contentType: environments.ContentTypePlain, + body: "Hello, Tester!", + }, { + method: http.MethodPost, + path: "/hello/postman", + statusCode: http.StatusOK, + contentType: environments.ContentTypePlain, + body: "Hello, Postman!", + }, { + method: http.MethodOptions, + path: "/path/does/not/exist", + statusCode: http.StatusNotFound, + body: "404 page not found", + }, + } + for i, test := range tests { + assert.Logf("test case #%d: %s %s", i, test.method, test.path) + wreq := wa.CreateRequest(test.method, test.path) + wresp := wreq.Do() + wresp.AssertStatusCodeEquals(test.statusCode) + if test.contentType != "" { + wresp.Header().AssertKeyValueEquals(environments.HeaderContentType, test.contentType) + } + if test.body != "" { + wresp.AssertBodyMatches(test.body) + } + } +} + +// TestHeaderCookies tests access to header and cookies. +func TestHeaderCookies(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + wa := StartWebAsserter(assert) + defer wa.Close() + + tests := []struct { + path string + header string + cookie string + }{ + { + path: "/header/cookies", + header: "foo", + cookie: "12345", + }, { + path: "/header/cookies", + header: "bar", + cookie: "98765", + }, + } + for i, test := range tests { + assert.Logf("test case #%d: GET %s", i, test.path) + wreq := wa.CreateRequest(http.MethodGet, test.path) + wreq.Header().Add("Header-In", test.header) + wreq.Header().Add("Cookie-In", test.cookie) + wresp := wreq.Do() + wresp.AssertStatusCodeEquals(http.StatusOK) + wresp.Header().AssertKeyValueEquals("Header-Out", test.header) + wresp.Cookies().AssertKeyValueEquals("Cookie-Out", test.cookie) + wresp.AssertBodyGrep(".*[Dd]one.*") + wresp.AssertBodyContains("!") + } +} + +//-------------------- +// WEB ASSERTER AND HANDLER +//-------------------- + +// StartTestServer initialises and starts the asserter for the tests. +func StartWebAsserter(assert *asserts.Asserts) *environments.WebAsserter { + wa := environments.NewWebAsserter(assert) + + wa.Handle("/hello/world/", MakeHelloWorldHandler(assert, "World")) + wa.Handle("/hello/tester/", MakeHelloWorldHandler(assert, "Tester")) + wa.Handle("/hello/postman/", MakeHelloWorldHandler(assert, "Postman")) + wa.Handle("/header/cookies/", MakeHeaderCookiesHandler(assert)) + return wa +} + +// MakeHelloWorldHandler creates a "Hello, World" handler. +func MakeHelloWorldHandler(assert *asserts.Asserts, who string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reply := "Hello, " + who + "!" + w.Header().Add(environments.HeaderContentType, environments.ContentTypePlain) + w.Write([]byte(reply)) + w.WriteHeader(http.StatusOK) + } +} + +// MakeHeaderCookiesHandler creates a handler for header and cookies. +func MakeHeaderCookiesHandler(assert *asserts.Asserts) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + headerOut := r.Header.Get("Header-In") + cookieOut := r.Header.Get("Cookie-In") + http.SetCookie(w, &http.Cookie{ + Name: "Cookie-Out", + Value: cookieOut, + }) + w.Header().Set(environments.HeaderContentType, environments.ContentTypePlain) + w.Header().Set("Header-Out", headerOut) + w.Write([]byte("Done!")) + w.WriteHeader(http.StatusOK) + } +} + +// EOF diff --git a/generators/doc.go b/generators/doc.go new file mode 100644 index 0000000..7a7d017 --- /dev/null +++ b/generators/doc.go @@ -0,0 +1,14 @@ +// Tideland Go Audit - Generators +// +// Copyright (C) 2013-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +// Package generators helps to quickly generate data needed for unit tests. +// The generation of all supported different types is based on a passed rand.Rand. +// When using the same value here the generated data will be the same when repeating +// tests. So generators.FixedRand() delivers such a fixed value. +package generators // import "tideland.dev/go/audit/generators" + +// EOF diff --git a/generators/generators.go b/generators/generators.go new file mode 100644 index 0000000..ffcc309 --- /dev/null +++ b/generators/generators.go @@ -0,0 +1,696 @@ +// Tideland Go Audit - Generators +// +// Copyright (C) 2013-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package generators // import "tideland.dev/go/audit/generators" + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "fmt" + "math/rand" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +//-------------------- +// CONSTANTS +//-------------------- + +// patterns is used by the pattern generator and contains the +// runes for a defined pattern identifier. +var patterns = map[rune]string{ + '0': "0123456789", + '1': "123456789", + 'o': "01234567", + 'O': "01234567", + 'h': "0123456789abcdef", + 'H': "0123456789ABCDEF", + 'a': "abcdefghijklmnopqrstuvwxyz", + 'A': "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + 'c': "bcdfghjklmnpqrstvwxyz", + 'C': "BCDFGHJKLMNPQRSTVWXYZ", + 'v': "aeiou", + 'V': "AEIOU", + 'z': "abcdefghijklmnopqrstuvwxyz0123456789", + 'Z': "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +} + +//-------------------- +// HELPERS +//-------------------- + +// SimpleRand returns a random number generator with a source using +// the the current time as seed. It's not the best random, but ok to +// generate test data. +func SimpleRand() *rand.Rand { + seed := time.Now().UnixNano() + source := rand.NewSource(seed) + return rand.New(source) +} + +// FixedRand returns a random number generator with a fixed source +// so that tests using the generate functions can be repeated with +// the same result. +func FixedRand() *rand.Rand { + source := rand.NewSource(42) + return rand.New(source) +} + +// ToUpperFirst returns the passed string with the first rune +// converted to uppercase. +func ToUpperFirst(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} + +// BuildEMail creates an e-mail address out of first and last +// name and the domain. +func BuildEMail(first, last, domain string) string { + valid := make(map[rune]bool) + for _, r := range "abcdefghijklmnopqrstuvwxyz0123456789-" { + valid[r] = true + } + name := func(in string) string { + out := []rune{} + for _, r := range strings.ToLower(in) { + if valid[r] { + out = append(out, r) + } + } + return string(out) + } + return name(first) + "." + name(last) + "@" + domain +} + +// BuildTime returns the current time plus or minus the passed +// offset formatted as string and as Time. The returned time is +// the parsed formatted one to avoid parsing troubles in tests. +func BuildTime(layout string, offset time.Duration) (string, time.Time) { + t := time.Now().Add(offset) + ts := t.Format(layout) + tp, err := time.Parse(layout, ts) + if err != nil { + panic("cannot build time: " + err.Error()) + } + return ts, tp +} + +// UUIDString creates a formatted string out of a generated pseudo UUID. +func UUIDString(uuid [16]byte) string { + return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]) +} + +//-------------------- +// GENERATOR +//-------------------- + +// Generator is responsible for generating different random data +// based on a random number generator. +type Generator struct { + rand *rand.Rand +} + +// New returns a new generator using the passed random number +// generator. +func New(rand *rand.Rand) *Generator { + return &Generator{rand} +} + +// Byte generates a byte between lo and hi including +// those values. +func (g *Generator) Byte(lo, hi byte) byte { + if lo == hi { + return lo + } + if lo > hi { + lo, hi = hi, lo + } + i := int(hi - lo) + n := byte(g.rand.Intn(i)) + return lo + n +} + +// Int generates an int between lo and hi including +// those values. +func (g *Generator) Int(lo, hi int) int { + if lo == hi { + return lo + } + if lo > hi { + lo, hi = hi, lo + } + n := g.rand.Intn(hi - lo + 1) + return lo + n +} + +// Ints generates a slice of random ints. +func (g *Generator) Ints(lo, hi, count int) []int { + ints := make([]int, count) + for i := 0; i < count; i++ { + ints[i] = g.Int(lo, hi) + } + return ints +} + +// Bytes generates a slice of random bytes. +func (g *Generator) Bytes(lo, hi byte, count int) []byte { + bytes := make([]byte, count) + for i := 0; i < count; i++ { + bytes[i] = g.Byte(lo, hi) + } + return bytes +} + +// UUID generates a 16 byte long random byte array. So it's +// no real UUID, even no v4, but it looks like. +func (g *Generator) UUID() [16]byte { + var uuid [16]byte + bytes := g.Bytes(0, 255, 16) + for i, b := range bytes { + uuid[i] = b + } + return uuid +} + +// Percent generates an int between 0 and 100. +func (g *Generator) Percent() int { + return g.Int(0, 100) +} + +// FlipCoin returns true if the internal generated percentage is +// equal or greater than the passed percentage. +func (g *Generator) FlipCoin(percent int) bool { + switch { + case percent > 100: + percent = 100 + case percent < 0: + percent = 0 + } + return g.Percent() >= percent +} + +// OneByteOf returns one of the passed bytes. +func (g *Generator) OneByteOf(values ...byte) byte { + if len(values) == 0 { + return 0 + } + i := g.Int(0, len(values)-1) + return values[i] +} + +// OneRuneOf returns one of the runes of the passed string. +func (g *Generator) OneRuneOf(values string) rune { + if len(values) == 0 { + return ' ' + } + runes := []rune(values) + i := g.Int(0, len(runes)-1) + return runes[i] +} + +// OneIntOf returns one of the passed ints. +func (g *Generator) OneIntOf(values ...int) int { + if len(values) == 0 { + return 0 + } + i := g.Int(0, len(values)-1) + return values[i] +} + +// OneStringOf returns one of the passed strings. +func (g *Generator) OneStringOf(values ...string) string { + if len(values) == 0 { + return "" + } + i := g.Int(0, len(values)-1) + return values[i] +} + +// OneDurationOf returns one of the passed durations. +func (g *Generator) OneDurationOf(values ...time.Duration) time.Duration { + i := g.Int(0, len(values)-1) + return values[i] +} + +// Word generates a random word. +func (g *Generator) Word() string { + return g.OneStringOf(words...) +} + +// Words generates a slice of random words +func (g *Generator) Words(count int) []string { + words := make([]string, count) + for i := 0; i < count; i++ { + words[i] = g.Word() + } + return words +} + +// LimitedWord generates a random word with a length between +// lo and hi. +func (g *Generator) LimitedWord(lo, hi int) string { + length := g.Int(lo, hi) + if length < MinWordLen { + length = MinWordLen + } + if length > MaxWordLen { + length = MaxWordLen + } + // Start anywhere in the list. + pos := g.Int(0, wordsLen) + for { + if pos >= wordsLen { + pos = 0 + } + if len(words[pos]) == length { + return words[pos] + } + pos++ + } +} + +// Pattern generates a string based on a pattern. Here different +// escape chars are replaced by according random chars while all +// others are left as they are. Escape chars start with a caret (^) +// followed by specializer. Those are: +// +// - ^ for a caret +// - 0 for a number between 0 and 9 +// - 1 for a number between 1 and 9 +// - o for an octal number +// - h for a hexadecimal number (lower-case) +// - H for a hexadecimal number (upper-case) +// - a for any char between a and z +// - A for any char between A and Z +// - c for a consonant (lower-case) +// - C for a consonant (upper-case) +// - v for a vowel (lower-case) +// - V for a vowel (upper-case) +func (g *Generator) Pattern(pattern string) string { + result := []rune{} + escaped := false + for _, pr := range pattern { + if !escaped { + if pr == '^' { + escaped = true + } else { + result = append(result, pr) + } + continue + } + // Escaped mode. + ar := pr + runes, ok := patterns[pr] + if ok { + ar = g.OneRuneOf(runes) + } + result = append(result, ar) + escaped = false + } + return string(result) +} + +// Sentence generates a sentence between 2 and 15 words +// and possibly containing commas. +func (g *Generator) Sentence() string { + count := g.Int(2, 15) + words := g.Words(count) + words[0] = ToUpperFirst(words[0]) + for i := 2; i < count-1; i++ { + if g.FlipCoin(80) { + words[i] += "," + } + } + return strings.Join(words, " ") + "." +} + +// SentenceWithNames works like Sentence but inserts randomly +// names of the passed set. They can be generated by NameSet(). +func (g *Generator) SentenceWithNames(names []string) string { + count := g.Int(2, 15) + words := []string{} + for _, word := range g.Words(count) { + if g.FlipCoin(90) { + words = append(words, g.OneStringOf(names...)) + } + words = append(words, word) + } + words[0] = ToUpperFirst(words[0]) + for i := 2; i < len(words)-1; i++ { + if g.FlipCoin(80) { + words[i] += "," + } + } + return strings.Join(words, " ") + "." +} + +// Paragraph generates a paragraph between 2 and 10 sentences. +func (g *Generator) Paragraph() string { + count := g.Int(2, 10) + sentences := make([]string, count) + for i := 0; i < count; i++ { + sentences[i] = g.Sentence() + } + return strings.Join(sentences, " ") +} + +// ParagraphWithNames workes like Paragraph but inserts randomly +// names of the passed set. They can be generated by NameSet(). +func (g *Generator) ParagraphWithNames(names []string) string { + count := g.Int(2, 10) + sentences := make([]string, count) + for i := 0; i < count; i++ { + sentences[i] = g.SentenceWithNames(names) + } + return strings.Join(sentences, " ") +} + +// Name generates a male or female name consisting out of first, +// middle and last name. +func (g *Generator) Name() (first, middle, last string) { + if g.FlipCoin(50) { + return g.FemaleName() + } + return g.MaleName() +} + +// Names generates a set of names to be used in other generators. +func (g *Generator) Names(count int) []string { + var names []string + for i := 0; i < count; i++ { + first, middle, last := g.Name() + if g.FlipCoin(50) { + names = append(names, first+" "+string(middle[0])+". "+last) + } else { + names = append(names, first+" "+last) + } + } + return names +} + +// MaleName generates a male name consisting out of first, middle +// and last name. +func (g *Generator) MaleName() (first, middle, last string) { + first = g.OneStringOf(maleFirstNames...) + middle = g.OneStringOf(maleFirstNames...) + if g.FlipCoin(80) { + first += "-" + g.OneStringOf(maleFirstNames...) + } else if g.FlipCoin(80) { + middle += "-" + g.OneStringOf(maleFirstNames...) + } + last = g.OneStringOf(lastNames...) + return +} + +// FemaleName generates a female name consisting out of first, middle +// and last name. +func (g *Generator) FemaleName() (first, middle, last string) { + first = g.OneStringOf(femaleFirstNames...) + middle = g.OneStringOf(femaleFirstNames...) + if g.FlipCoin(80) { + first += "-" + g.OneStringOf(femaleFirstNames...) + } else if g.FlipCoin(80) { + middle += "-" + g.OneStringOf(femaleFirstNames...) + } + last = g.OneStringOf(lastNames...) + return +} + +// Domain generates domain out of name and top level domain. +func (g *Generator) Domain() string { + tld := g.OneStringOf(topLevelDomains...) + if g.FlipCoin(80) { + return g.LimitedWord(3, 5) + "-" + g.LimitedWord(3, 5) + "." + tld + } + return g.LimitedWord(3, 10) + "." + tld +} + +// URL generates a http, https or ftp URL, some of the leading +// to a file. +func (g *Generator) URL() string { + part := func() string { + return g.LimitedWord(2, 8) + } + start := g.OneStringOf("http://www.", "http://blog.", "https://www.", "ftp://") + ext := g.OneStringOf("html", "php", "jpg", "mp3", "txt") + variant := g.Percent() + switch { + case variant < 20: + return start + part() + "." + g.Domain() + "/" + part() + "." + ext + case variant > 80: + return start + part() + "." + g.Domain() + "/" + part() + "/" + part() + "." + ext + default: + return start + part() + "." + g.Domain() + } +} + +// EMail returns a random e-mail address. +func (g *Generator) EMail() string { + if g.FlipCoin(50) { + first, _, last := g.MaleName() + return BuildEMail(first, last, g.Domain()) + } + first, _, last := g.FemaleName() + return BuildEMail(first, last, g.Domain()) +} + +// Duration generates a duration between lo and hi including +// those values. +func (g *Generator) Duration(lo, hi time.Duration) time.Duration { + if lo == hi { + return lo + } + if lo > hi { + lo, hi = hi, lo + } + n := g.rand.Int63n(int64(hi) - int64(lo) + 1) + return lo + time.Duration(n) +} + +// SleepOneOf chooses randomely one of the passed durations +// and lets the goroutine sleep for this time. +func (g *Generator) SleepOneOf(sleeps ...time.Duration) time.Duration { + sleep := g.OneDurationOf(sleeps...) + time.Sleep(sleep) + return sleep +} + +// Time generates a time between the given one and that time +// plus the given duration. The result will have the passed +// location. +func (g *Generator) Time(loc *time.Location, base time.Time, dur time.Duration) time.Time { + base = base.UTC() + return base.Add(g.Duration(0, dur)).In(loc) +} + +//-------------------- +// GENERATOR DATA +//-------------------- + +// words is a list of words based on lorem ipsum and own extensions. +var words = []string{ + "a", "ac", "accumsan", "accusam", "accusantium", "ad", "adipiscing", + "alias", "aliquam", "aliquet", "aliquip", "aliquyam", "amet", "aenean", + "ante", "aperiam", "arcu", "assum", "at", "auctor", "augue", "aut", "autem", + "bibendum", "blandit", "blanditiis", + "clita", "commodo", "condimentum", "congue", "consectetuer", "consequat", + "consequatur", "consequuntur", "consetetur", "convallis", "cras", "cubilia", + "culpa", "cum", "curabitur", "curae", "cursus", + "dapibus", "delectus", "delenit", "diam", "dictum", "dictumst", "dignissim", "dis", + "dolor", "dolore", "dolores", "doloremque", "doming", "donec", "dui", "duis", "duo", + "ea", "eaque", "earum", "egestas", "eget", "eirmod", "eleifend", "elementum", "elit", + "elitr", "enim", "eos", "erat", "eros", "errare", "error", "esse", "est", "et", "etiam", + "eu", "euismod", "eum", "ex", "exerci", "exercitationem", + "facer", "facili", "facilisis", "fames", "faucibus", "felis", "fermentum", + "feugait", "feugiat", "fringilla", "fuga", "fusce", + "gravida", "gubergren", + "habitant", "habitasse", "hac", "harum", "hendrerit", "hic", + "iaculis", "id", "illum", "illo", "imperdiet", "in", "integer", "interdum", + "invidunt", "ipsa", "ipsum", "iriure", "iusto", + "justo", + "kasd", "kuga", + "labore", "lacinia", "lacus", "laoreet", "laudantium", "lectus", "leo", "liber", + "libero", "ligula", "lobortis", "laboriosam", "lorem", "luctus", "luptatum", + "maecenas", "magna", "magni", "magnis", "malesuada", "massa", "mattis", "mauris", + "mazim", "mea", "metus", "mi", "minim", "molestie", "mollis", "montes", "morbi", "mus", + "nam", "nascetur", "natoque", "nec", "neque", "nesciunt", "netus", "nibh", "nihil", + "nisi", "nisl", "no", "nobis", "non", "nonummy", "nonumy", "nostrud", "nulla", + "nullam", "nunc", + "odio", "odit", "officia", "option", "orci", "ornare", + "parturient", "pede", "pellentesque", "penatibus", "perfendis", "perspiciatis", + "pharetra", "phasellus", "placerat", "platea", "porta", "porttitor", "possim", + "posuere", "praesent", "praesentium", "pretium", "primis", "proin", "pulvinar", + "purus", + "quam", "qui", "quia", "quis", "quisque", "quod", + "rebum", "rhoncus", "riduculus", "risus", "rutrum", + "sadipscing", "sagittis", "sanctus", "sapien", "scelerisque", "sea", "sed", + "sem", "semper", "senectus", "sit", "sociis", "sodales", "sollicitudin", "soluta", + "stet", "suscipit", "suspendisse", + "takimata", "tation", "te", "tellus", "tempor", "tempora", "temporibus", "tempus", + "tincidunt", "tortor", "totam", "tristique", "turpis", + "ullam", "ullamcorper", "ultrices", "ultricies", "urna", "ut", + "varius", "vehicula", "vel", "velit", "venenatis", "veniam", "vero", "vestibulum", + "vitae", "vivamus", "viverra", "voluptua", "volutpat", "voluptatem", "vulputate", + "voluptatem", + "wisi", "wiskaleborium", + "xantippe", "xeon", + "yodet", "yggdrasil", + "zypres", "zyril", +} + +// wordsLen is the length of the word list. +var wordsLen = len(words) + +const ( + // MinWordLen is the length of the shortest word. + MinWordLen = 1 + + // MaxWordLen is the length of the longest word. + MaxWordLen = 14 +) + +// maleFirstNames is a list of popular male first names. +var maleFirstNames = []string{ + "Jacob", "Michael", "Joshua", "Matthew", "Ethan", "Andrew", "Daniel", + "Anthony", "Christopher", "Joseph", "William", "Alexander", "Ryan", "David", + "Nicholas", "Tyler", "James", "John", "Jonathan", "Nathan", "Samuel", + "Christian", "Noah", "Dylan", "Benjamin", "Logan", "Brandon", "Gabriel", + "Zachary", "Jose", "Elijah", "Angel", "Kevin", "Jack", "Caleb", "Justin", + "Austin", "Evan", "Robert", "Thomas", "Luke", "Mason", "Aidan", "Jackson", + "Isaiah", "Jordan", "Gavin", "Connor", "Aiden", "Isaac", "Jason", "Cameron", + "Hunter", "Jayden", "Juan", "Charles", "Aaron", "Lucas", "Luis", "Owen", + "Landon", "Diego", "Brian", "Adam", "Adrian", "Kyle", "Eric", "Ian", "Nathaniel", + "Carlos", "Alex", "Bryan", "Jesus", "Julian", "Sean", "Carter", "Hayden", + "Jeremiah", "Cole", "Brayden", "Wyatt", "Chase", "Steven", "Timothy", "Dominic", + "Sebastian", "Xavier", "Jaden", "Jesse", "Devin", "Seth", "Antonio", "Richard", + "Miguel", "Colin", "Cody", "Alejandro", "Caden", "Blake", "Carson", +} + +// maleFirstNames is a list of popular female first names. +var femaleFirstNames = []string{ + "Emily", "Emma", "Madison", "Abigail", "Olivia", "Isabella", "Hannah", + "Samantha", "Ava", "Ashley", "Sophia", "Elizabeth", "Alexis", "Grace", + "Sarah", "Alyssa", "Mia", "Natalie", "Chloe", "Brianna", "Lauren", "Ella", + "Anna", "Taylor", "Kayla", "Hailey", "Jessica", "Victoria", "Jasmine", "Sydney", + "Julia", "Destiny", "Morgan", "Kaitlyn", "Savannah", "Katherine", "Alexandra", + "Rachel", "Lily", "Megan", "Kaylee", "Jennifer", "Angelina", "Makayla", "Allison", + "Brooke", "Maria", "Trinity", "Lillian", "Mackenzie", "Faith", "Sofia", "Riley", + "Haley", "Gabrielle", "Nicole", "Kylie", "Katelyn", "Zoe", "Paige", "Gabriella", + "Jenna", "Kimberly", "Stephanie", "Alexa", "Avery", "Andrea", "Leah", "Madeline", + "Nevaeh", "Evelyn", "Maya", "Mary", "Michelle", "Jada", "Sara", "Audrey", + "Brooklyn", "Vanessa", "Amanda", "Ariana", "Rebecca", "Caroline", "Amelia", + "Mariah", "Jordan", "Jocelyn", "Arianna", "Isabel", "Marissa", "Autumn", "Melanie", + "Aaliyah", "Gracie", "Claire", "Isabelle", "Molly", "Mya", "Diana", "Katie", +} + +// lastNames is a list of popular last names. +var lastNames = []string{ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Miller", "Davis", "Garcia", + "Rodriguez", "Wilson", "Martinez", "Anderson", "Taylor", "Thomas", "Hernandez", + "Moore", "Martin", "Jackson", "Thompson", "White", "Lopez", "Lee", "Gonzalez", + "Harris", "Clark", "Lewis", "Robinson", "Walker", "Perez", "Hall", "Young", + "Allen", "Sanchez", "Wright", "King", "Scott", "Green", "Baker", "Adams", "Nelson", + "Hill", "Ramirez", "Campbell", "Mitchell", "Roberts", "Carter", "Phillips", "Evans", + "Turner", "Torres", "Parker", "Collins", "Edwards", "Stewart", "Flores", "Morris", + "Nguyen", "Murphy", "Rivera", "Cook", "Rogers", "Morgan", "Peterson", "Cooper", + "Reed", "Bailey", "Bell", "Gomez", "Kelly", "Howard", "Ward", "Cox", "Diaz", + "Richardson", "Wood", "Watson", "Brooks", "Bennett", "Gray", "James", "Reyes", + "Cruz", "Hughes", "Price", "Myers", "Long", "Foster", "Sanders", "Ross", "Morales", + "Powell", "Sullivan", "Russell", "Ortiz", "Jenkins", "Gutierrez", "Perry", "Butler", + "Barnes", "Fisher", "Henderson", "Coleman", "Simmons", "Patterson", "Jordan", + "Reynolds", "Hamilton", "Graham", "Kim", "Gonzales", "Alexander", "Ramos", "Wallace", + "Griffin", "West", "Cole", "Hayes", "Chavez", "Gibson", "Bryant", "Ellis", "Stevens", + "Murray", "Ford", "Marshall", "Owens", "McDonald", "Harrison", "Ruiz", "Kennedy", + "Wells", "Alvarez", "Woods", "Mendoza", "Castillo", "Olson", "Webb", "Washington", + "Tucker", "Freeman", "Burns", "Henry", "Vasquez", "Snyder", "Simpson", "Crawford", + "Jimenez", "Porter", "Mason", "Shaw", "Gordon", "Wagner", "Hunter", "Romero", + "Hicks", "Dixon", "Hunt", "Palmer", "Robertson", "Black", "Holmes", "Stone", + "Meyer", "Boyd", "Mills", "Warren", "Fox", "Rose", "Rice", "Moreno", "Schmidt", + "Patel", "Ferguson", "Nichols", "Herrera", "Medina", "Ryan", "Fernandez", "Weaver", + "Daniels", "Stephens", "Gardner", "Payne", "Kelley", "Dunn", "Pierce", "Arnold", + "Tran", "Spencer", "Peters", "Hawkins", "Grant", "Hansen", "Castro", "Hoffman", + "Hart", "Elliott", "Cunningham", "Knight", "Bradley", "Carroll", "Hudson", "Duncan", + "Armstrong", "Berry", "Andrews", "Johnston", "Ray", "Lane", "Riley", "Carpenter", + "Perkins", "Aguilar", "Silva", "Richards", "Willis", "Matthews", "Chapman", + "Lawrence", "Garza", "Vargas", "Watkins", "Wheeler", "Larson", "Carlson", "Harper", + "George", "Greene", "Burke", "Guzman", "Morrison", "Munoz", "Jacobs", "Obrien", + "Lawson", "Franklin", "Lynch", "Bishop", "Carr", "Salazar", "Austin", "Mendez", + "Gilbert", "Jensen", "Williamson", "Montgomery", "Harvey", "Oliver", "Howell", + "Dean", "Hanson", "Weber", "Garrett", "Sims", "Burton", "Fuller", "Soto", "McCoy", + "Welch", "Chen", "Schultz", "Walters", "Reid", "Fields", "Walsh", "Little", "Fowler", + "Bowman", "Davidson", "May", "Day", "Schneider", "Newman", "Brewer", "Lucas", "Holland", + "Wong", "Banks", "Santos", "Curtis", "Pearson", "Delgado", "Valdez", "Pena", "Rios", + "Douglas", "Sandoval", "Barrett", "Hopkins", "Keller", "Guerrero", "Stanley", "Bates", + "Alvarado", "Beck", "Ortega", "Wade", "Estrada", "Contreras", "Barnett", "Caldwell", + "Santiago", "Lambert", "Powers", "Chambers", "Nunez", "Craig", "Leonard", "Lowe", "Rhodes", + "Byrd", "Gregory", "Shelton", "Frazier", "Becker", "Maldonado", "Fleming", "Vega", + "Sutton", "Cohen", "Jennings", "Parks", "McDaniel", "Watts", "Barker", "Norris", + "Vaughn", "Vazquez", "Holt", "Schwartz", "Steele", "Benson", "Neal", "Dominguez", + "Horton", "Terry", "Wolfe", "Hale", "Lyons", "Graves", "Haynes", "Miles", "Park", + "Warner", "Padilla", "Bush", "Thornton", "McCarthy", "Mann", "Zimmerman", "Erickson", + "Fletcher", "McKinney", "Page", "Dawson", "Joseph", "Marquez", "Reeves", "Klein", + "Espinoza", "Baldwin", "Moran", "Love", "Robbins", "Higgins", "Ball", "Cortez", "Le", + "Griffith", "Bowen", "Sharp", "Cummings", "Ramsey", "Hardy", "Swanson", "Barber", + "Acosta", "Luna", "Chandler", "Blair", "Daniel", "Cross", "Simon", "Dennis", "Oconnor", + "Quinn", "Gross", "Navarro", "Moss", "Fitzgerald", "Doyle", "McLaughlin", "Rojas", + "Rodgers", "Stevenson", "Singh", "Yang", "Figueroa", "Harmon", "Newton", "Paul", + "Manning", "Garner", "McGee", "Reese", "Francis", "Burgess", "Adkins", "Goodman", + "Curry", "Brady", "Christensen", "Potter", "Walton", "Goodwin", "Mullins", "Molina", + "Webster", "Fischer", "Campos", "Avila", "Sherman", "Todd", "Chang", "Blake", "Malone", + "Wolf", "Hodges", "Juarez", "Gill", "Farmer", "Hines", "Gallagher", "Duran", "Hubbard", + "Cannon", "Miranda", "Wang", "Saunders", "Tate", "Mack", "Hammond", "Carrillo", + "Townsend", "Wise", "Ingram", "Barton", "Mejia", "Ayala", "Schroeder", "Hampton", + "Rowe", "Parsons", "Frank", "Waters", "Strickland", "Osborne", "Maxwell", "Chan", + "Deleon", "Norman", "Harrington", "Casey", "Patton", "Logan", "Bowers", "Mueller", + "Glover", "Floyd", "Hartman", "Buchanan", "Cobb", "French", "Kramer", "McCormick", + "Clarke", "Tyler", "Gibbs", "Moody", "Conner", "Sparks", "McGuire", "Leon", "Bauer", + "Norton", "Pope", "Flynn", "Hogan", "Robles", "Salinas", "Yates", "Lindsey", "Lloyd", + "Marsh", "McBride", "Owen", "Solis", "Pham", "Lang", "Pratt", "Lara", "Brock", "Ballard", + "Trujillo", "Shaffer", "Drake", "Roman", "Aguirre", "Morton", "Stokes", "Lamb", "Pacheco", + "Patrick", "Cochran", "Shepherd", "Cain", "Burnett", "Hess", "Li", "Cervantes", "Olsen", + "Briggs", "Ochoa", "Cabrera", "Velasquez", "Montoya", "Roth", "Meyers", "Cardenas", "Fuentes", + "Weiss", "Hoover", "Wilkins", "Nicholson", "Underwood", "Short", "Carson", "Morrow", "Colon", + "Holloway", "Summers", "Bryan", "Petersen", "McKenzie", "Serrano", "Wilcox", "Carey", "Clayton", + "Poole", "Calderon", "Gallegos", "Greer", "Rivas", "Guerra", "Decker", "Collier", "Wall", + "Whitaker", "Bass", "Flowers", "Davenport", "Conley", "Houston", "Huff", "Copeland", "Hood", + "Monroe", "Massey", "Roberson", "Combs", "Franco", "Larsen", "Pittman", "Randall", "Skinner", + "Wilkinson", "Kirby", "Cameron", "Bridges", "Anthony", "Richard", "Kirk", "Bruce", "Singleton", + "Mathis", "Bradford", "Boone", "Abbott", "Charles", "Allison", "Sweeney", "Atkinson", "Horn", + "Jefferson", "Rosales", "York", "Christian", "Phelps", "Farrell", "Castaneda", "Nash", + "Dickerson", "Bond", "Wyatt", "Foley", "Chase", "Gates", "Vincent", "Mathews", "Hodge", + "Garrison", "Trevino", "Villarreal", "Heath", "Dalton", "Valencia", "Callahan", "Hensley", + "Atkins", "Huffman", "Roy", "Boyer", "Shields", "Lin", "Hancock", "Grimes", "Glenn", "Cline", + "Delacruz", "Camacho", "Dillon", "Parrish", "O'Neill", "Melton", "Booth", "Kane", "Berg", + "Harrell", "Pitts", "Savage", "Wiggins", "Brennan", "Salas", "Marks", "Russo", "Sawyer", + "Baxter", "Golden", "Hutchinson", "Liu", "Walter", "McDowell", "Wiley", "Rich", "Humphrey", + "Johns", "Koch", "Suarez", "Hobbs", "Beard", "Gilmore", "Ibarra", "Keith", "Macias", "Khan", + "Andrade", "Ware", "Stephenson", "Henson", "Wilkerson", "Dyer", "McClure", "Blackwell", + "Mercado", "Tanner", "Eaton", "Clay", "Barron", "Beasley", "O'Neal", "Preston", "Small", + "Wu", "Zamora", "Macdonald", "Vance", "Snow", "McClain", "Stafford", "Orozco", "Barry", + "English", "Shannon", "Kline", "Jacobson", "Woodard", "Huang", "Kemp", "Mosley", "Prince", + "Merritt", "Hurst", "Villanueva", "Roach", "Nolan", "Lam", "Yoder", "McCullough", "Lester", + "Santana", "Valenzuela", "Winters", "Barrera", "Leach", "Orr", "Berger", "McKee", "Strong", + "Conway", "Stein", "Whitehead", "Bullock", "Escobar", "Knox", "Meadows", "Solomon", "Velez", + "Odonnell", "Kerr", "Stout", "Blankenship", "Browning", "Kent", "Lozano", "Bartlett", "Pruitt", + "Buck", "Barr", "Gaines", "Durham", "Gentry", "McIntyre", "Sloan", "Melendez", "Rocha", "Herman", + "Sexton", "Moon", "Hendricks", "Rangel", +} + +// topLevelDomains is a number of existing top level domains. +var topLevelDomains = []string{"asia", "at", "au", "biz", "ch", "cn", "com", "de", "es", + "eu", "fr", "gr", "guru", "info", "it", "mobi", "name", "net", "org", "pl", "ru", + "tel", "tv", "uk", "us", +} + +// EOF diff --git a/generators/generators_test.go b/generators/generators_test.go new file mode 100644 index 0000000..7124014 --- /dev/null +++ b/generators/generators_test.go @@ -0,0 +1,378 @@ +// Tideland Go Audit - Generators - Unit Tests +// +// Copyright (C) 2013-2020 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the New BSD license. + +package generators_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "fmt" + "strings" + "testing" + "time" + + "tideland.dev/go/audit/asserts" + "tideland.dev/go/audit/generators" +) + +//-------------------- +// TESTS +//-------------------- + +// TestBuildDate tests the generation of dates. +func TestBuildDate(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + layouts := []string{ + time.ANSIC, + time.UnixDate, + time.RubyDate, + time.RFC822, + time.RFC822Z, + time.RFC850, + time.RFC1123, + time.RFC1123Z, + time.RFC3339, + time.RFC3339Nano, + time.Kitchen, + time.Stamp, + time.StampMilli, + time.StampMicro, + time.StampNano, + } + + for _, layout := range layouts { + ts, t := generators.BuildTime(layout, 0) + tsp, err := time.Parse(layout, ts) + assert.Nil(err) + assert.Equal(t, tsp) + + ts, t = generators.BuildTime(layout, -30*time.Minute) + tsp, err = time.Parse(layout, ts) + assert.Nil(err) + assert.Equal(t, tsp) + + ts, t = generators.BuildTime(layout, time.Hour) + tsp, err = time.Parse(layout, ts) + assert.Nil(err) + assert.Equal(t, tsp) + } +} + +// TestBytes tests the generation of bytes. +func TestBytes(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + // Test individual bytes. + for i := 0; i < 10000; i++ { + lo := gen.Byte(0, 255) + hi := gen.Byte(0, 255) + n := gen.Byte(lo, hi) + if hi < lo { + lo, hi = hi, lo + } + assert.True(lo <= n && n <= hi) + } + + // Test byte slices. + ns := gen.Bytes(1, 200, 1000) + assert.Length(ns, 1000) + for _, n := range ns { + assert.True(n >= 1 && n <= 200) + } + + // Test UUIDs. + for i := 0; i < 10000; i++ { + uuid := gen.UUID() + assert.Length(uuid, 16) + } +} + +// TestInts tests the generation of ints. +func TestInts(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + // Test individual ints. + for i := 0; i < 10000; i++ { + lo := gen.Int(-100, 100) + hi := gen.Int(-100, 100) + n := gen.Int(lo, hi) + if hi < lo { + lo, hi = hi, lo + } + assert.True(lo <= n && n <= hi) + } + + // Test int slices. + ns := gen.Ints(0, 500, 10000) + assert.Length(ns, 10000) + for _, n := range ns { + assert.True(n >= 0 && n <= 500) + } + + // Test the generation of percent. + for i := 0; i < 10000; i++ { + p := gen.Percent() + assert.True(p >= 0 && p <= 100) + } + + // Test the flipping of coins. + ct := 0 + cf := 0 + for i := 0; i < 10000; i++ { + c := gen.FlipCoin(50) + if c { + ct++ + } else { + cf++ + } + } + assert.About(float64(ct), float64(cf), 500) +} + +// TestOneOf tests the generation of selections. +func TestOneOf(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + for i := 0; i < 10000; i++ { + b := gen.OneByteOf(1, 2, 3, 4, 5) + assert.True(b >= 1 && b <= 5) + + r := gen.OneRuneOf("abcdef") + assert.True(r >= 'a' && r <= 'f') + + n := gen.OneIntOf(1, 2, 3, 4, 5) + assert.True(n >= 1 && n <= 5) + + s := gen.OneStringOf("one", "two", "three", "four", "five") + assert.Substring(s, "one/two/three/four/five") + + d := gen.OneDurationOf(1*time.Second, 2*time.Second, 3*time.Second) + assert.True(d >= 1*time.Second && d <= 3*time.Second) + } +} + +// TestWords tests the generation of words. +func TestWords(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + // Test single words. + for i := 0; i < 10000; i++ { + w := gen.Word() + for _, r := range w { + assert.True(r >= 'a' && r <= 'z') + } + } + + // Test limited words. + for i := 0; i < 10000; i++ { + lo := gen.Int(generators.MinWordLen, generators.MaxWordLen) + hi := gen.Int(generators.MinWordLen, generators.MaxWordLen) + w := gen.LimitedWord(lo, hi) + wl := len(w) + if hi < lo { + lo, hi = hi, lo + } + assert.True(lo <= wl && wl <= hi, info("WL %d LO %d HI %d", wl, lo, hi)) + } +} + +// TestPattern tests the generation based on patterns. +func TestPattern(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + assertPattern := func(pattern, runes string) { + set := make(map[rune]bool) + for _, r := range runes { + set[r] = true + } + for i := 0; i < 10; i++ { + result := gen.Pattern(pattern) + for _, r := range result { + assert.True(set[r], pattern, result, runes) + } + } + } + + assertPattern("^^", "^") + assertPattern("^0^0^0^0^0", "0123456789") + assertPattern("^1^1^1^1^1", "123456789") + assertPattern("^o^o^o^o^o", "01234567") + assertPattern("^h^h^h^h^h", "0123456789abcdef") + assertPattern("^H^H^H^H^H", "0123456789ABCDEF") + assertPattern("^a^a^a^a^a", "abcdefghijklmnopqrstuvwxyz") + assertPattern("^A^A^A^A^A", "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + assertPattern("^c^c^c^c^c", "bcdfghjklmnpqrstvwxyz") + assertPattern("^C^C^C^C^C", "BCDFGHJKLMNPQRSTVWXYZ") + assertPattern("^v^v^v^v^v", "aeiou") + assertPattern("^V^V^V^V^V", "AEIOU") + assertPattern("^z^z^z^z^z", "abcdefghijklmnopqrstuvwxyz0123456789") + assertPattern("^Z^Z^Z^Z^Z", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + assertPattern("^1^0.^0^0^0,^0^0 €", "0123456789 .,€") +} + +// TestText tests the generation of text. +func TestText(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + names := gen.Names(4) + + for i := 0; i < 10000; i++ { + s := gen.Sentence() + ws := strings.Split(s, " ") + lws := len(ws) + assert.True(2 <= lws && lws <= 15, info("S: %v SL: %d", s, lws)) + assert.True('A' <= s[0] && s[0] <= 'Z', info("SUC: %v", s[0])) + } + + for i := 0; i < 10; i++ { + s := gen.SentenceWithNames(names) + assert.NotEmpty(s) + } + + for i := 0; i < 10000; i++ { + p := gen.Paragraph() + ss := strings.Split(p, ". ") + lss := len(ss) + assert.True(2 <= lss && lss <= 10, info("PL: %d", lss)) + for _, s := range ss { + ws := strings.Split(s, " ") + lws := len(ws) + assert.True(2 <= lws && lws <= 15, info("S: %v PSL: %d", s, lws)) + assert.True('A' <= s[0] && s[0] <= 'Z', info("PSUC: %v", s[0])) + } + } + + for i := 0; i < 10; i++ { + s := gen.ParagraphWithNames(names) + assert.NotEmpty(s) + } +} + +// TestName tests the generation of names. +func TestName(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + assert.Equal(generators.ToUpperFirst("yadda"), "Yadda") + + for i := 0; i < 10000; i++ { + first, middle, last := gen.Name() + + assert.Match(first, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(middle, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(last, `[A-Z]['a-zA-Z]+`) + + first, middle, last = gen.MaleName() + + assert.Match(first, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(middle, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(last, `[A-Z]['a-zA-Z]+`) + + first, middle, last = gen.FemaleName() + + assert.Match(first, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(middle, `[A-Z][a-z]+(-[A-Z][a-z]+)?`) + assert.Match(last, `[A-Z]['a-zA-Z]+`) + + count := gen.Int(0, 5) + names := gen.Names(count) + + assert.Length(names, count) + for _, name := range names { + assert.Match(name, `[A-Z][a-z]+(-[A-Z][a-z]+)?\s([A-Z]\.\s)?[A-Z]['a-zA-Z]+`) + } + } +} + +// TestDomain tests the generation of domains. +func TestDomain(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + for i := 0; i < 00100; i++ { + domain := gen.Domain() + + assert.Match(domain, `^[a-z0-9.-]+\.[a-z]{2,4}$`) + } +} + +// TestURL tests the generation of URLs. +func TestURL(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + for i := 0; i < 10000; i++ { + url := gen.URL() + + assert.Match(url, `(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`) + } +} + +// TestEMail tests the generation of e-mail addresses. +func TestEMail(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + for i := 0; i < 10000; i++ { + addr := gen.EMail() + + assert.Match(addr, `^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$`) + } +} + +// TestTimes tests the generation of durations and times. +func TestTimes(t *testing.T) { + assert := asserts.NewTesting(t, asserts.FailStop) + gen := generators.New(generators.FixedRand()) + + for i := 0; i < 10000; i++ { + // Test durations. + lo := gen.Duration(time.Second, time.Minute) + hi := gen.Duration(time.Second, time.Minute) + d := gen.Duration(lo, hi) + if hi < lo { + lo, hi = hi, lo + } + assert.True(lo <= d && d <= hi, "High / Low") + + // Test times. + loc := time.Local + now := time.Now() + dur := gen.Duration(24*time.Hour, 30*24*time.Hour) + t := gen.Time(loc, now, dur) + assert.True(t.Equal(now) || t.After(now), "Equal or after now") + assert.True(t.Before(now.Add(dur)) || t.Equal(now.Add(dur)), "Before or equal now plus duration") + } + + sleeps := map[int]time.Duration{ + 1: 1 * time.Millisecond, + 2: 2 * time.Millisecond, + 3: 3 * time.Millisecond, + 4: 4 * time.Millisecond, + 5: 5 * time.Millisecond, + } + for i := 0; i < 1000; i++ { + sleep := gen.SleepOneOf(sleeps[1], sleeps[2], sleeps[3], sleeps[4], sleeps[5]) + s := int(sleep) / 1000000 + _, ok := sleeps[s] + assert.True(ok, "Chosen duration is one the arguments") + } +} + +//-------------------- +// HELPER +//-------------------- + +var info = fmt.Sprintf + +// EOF diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..358f374 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tideland.dev/go/audit + +go 1.13