mirror of https://github.com/docker/cli.git
Replace gotestyourself by gotest.tools
github.com/gotestyourself/gotestyourself moved to gotest.tools with version 2.0.0. Moving to that one, bumping it to v2.1.0. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
aac3124794
commit
dd9478a1f7
|
@ -29,7 +29,7 @@ github.com/google/gofuzz 44d81051d367757e1c7c6a5a86423ece9afcf63c
|
|||
github.com/googleapis/gnostic e4f56557df6250e1945ee6854f181ce4e1c2c646
|
||||
github.com/gorilla/context v1.1
|
||||
github.com/gorilla/mux v1.1
|
||||
github.com/gotestyourself/gotestyourself cf3a5ab914a2efa8bc838d09f5918c1d44d02909
|
||||
gotest.tools v2.1.0
|
||||
github.com/go-openapi/jsonpointer 46af16f9f7b149af66e5d1bd010e3574dc06de98
|
||||
github.com/go-openapi/jsonreference 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
|
||||
github.com/go-openapi/spec 6aced65f8501fe1217321abf0749d354824ba2ff
|
||||
|
@ -56,7 +56,6 @@ github.com/opencontainers/image-spec v1.0.1
|
|||
github.com/opencontainers/runc 4fc53a81fb7c994640722ac585fa9ca548971871
|
||||
github.com/peterbourgon/diskv 5f041e8faa004a95c88a202771f4cc3e991971e6
|
||||
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/prometheus/client_golang 52437c81da6b127a9925d17eb3a382a2e5fd395e
|
||||
github.com/prometheus/client_model fa8ad6fec33561be4280a8f0514318c79d7f6cb6
|
||||
github.com/prometheus/common ebdfc6da46522d58825777cf1f90490a5b1ef1d8
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# Go Test Yourself
|
||||
|
||||
A collection of packages compatible with `go test` to support common testing
|
||||
patterns.
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/gotestyourself/gotestyourself?status.svg)](https://godoc.org/github.com/gotestyourself/gotestyourself)
|
||||
[![CircleCI](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master)
|
||||
[![Go Reportcard](https://goreportcard.com/badge/github.com/gotestyourself/gotestyourself)](https://goreportcard.com/report/github.com/gotestyourself/gotestyourself)
|
||||
|
||||
|
||||
## Packages
|
||||
|
||||
* [assert](http://godoc.org/github.com/gotestyourself/gotestyourself/assert) -
|
||||
compare values and fail the test when the comparison fails
|
||||
* [env](http://godoc.org/github.com/gotestyourself/gotestyourself/env) -
|
||||
test code that uses environment variables
|
||||
* [fs](http://godoc.org/github.com/gotestyourself/gotestyourself/fs) -
|
||||
create test files and directories
|
||||
* [golden](http://godoc.org/github.com/gotestyourself/gotestyourself/golden) -
|
||||
compare large multi-line strings
|
||||
* [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) -
|
||||
execute binaries and test the output
|
||||
* [poll](http://godoc.org/github.com/gotestyourself/gotestyourself/poll) -
|
||||
test asynchronous code by polling until a desired state is reached
|
||||
* [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) -
|
||||
skip tests based on conditions
|
||||
* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) -
|
||||
a program to summarize `go test` output and test failures
|
||||
|
||||
## Related
|
||||
|
||||
* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces
|
||||
* [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time`
|
|
@ -1,50 +0,0 @@
|
|||
go-difflib
|
||||
==========
|
||||
|
||||
[![Build Status](https://travis-ci.org/pmezard/go-difflib.png?branch=master)](https://travis-ci.org/pmezard/go-difflib)
|
||||
[![GoDoc](https://godoc.org/github.com/pmezard/go-difflib/difflib?status.svg)](https://godoc.org/github.com/pmezard/go-difflib/difflib)
|
||||
|
||||
Go-difflib is a partial port of python 3 difflib package. Its main goal
|
||||
was to make unified and context diff available in pure Go, mostly for
|
||||
testing purposes.
|
||||
|
||||
The following class and functions (and related tests) have be ported:
|
||||
|
||||
* `SequenceMatcher`
|
||||
* `unified_diff()`
|
||||
* `context_diff()`
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ go get github.com/pmezard/go-difflib/difflib
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
Diffs are configured with Unified (or ContextDiff) structures, and can
|
||||
be output to an io.Writer or returned as a string.
|
||||
|
||||
```Go
|
||||
diff := UnifiedDiff{
|
||||
A: difflib.SplitLines("foo\nbar\n"),
|
||||
B: difflib.SplitLines("foo\nbaz\n"),
|
||||
FromFile: "Original",
|
||||
ToFile: "Current",
|
||||
Context: 3,
|
||||
}
|
||||
text, _ := GetUnifiedDiffString(diff)
|
||||
fmt.Printf(text)
|
||||
```
|
||||
|
||||
would output:
|
||||
|
||||
```
|
||||
--- Original
|
||||
+++ Current
|
||||
@@ -1,3 +1,3 @@
|
||||
foo
|
||||
-bar
|
||||
+baz
|
||||
```
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# gotest.tools
|
||||
|
||||
A collection of packages to augment `testing` and support common patterns.
|
||||
|
||||
[![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://godoc.org/gotest.tools)
|
||||
[![CircleCI](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master)
|
||||
[![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](https://goreportcard.com/report/gotest.tools)
|
||||
|
||||
|
||||
## Packages
|
||||
|
||||
* [assert](http://godoc.org/gotest.tools/assert) -
|
||||
compare values and fail the test when a comparison fails
|
||||
* [env](http://godoc.org/gotest.tools/env) -
|
||||
test code which uses environment variables
|
||||
* [fs](http://godoc.org/gotest.tools/fs) -
|
||||
create temporary files and compare a filesystem tree to an expected value
|
||||
* [golden](http://godoc.org/gotest.tools/golden) -
|
||||
compare large multi-line strings against values frozen in golden files
|
||||
* [icmd](http://godoc.org/gotest.tools/icmd) -
|
||||
execute binaries and test the output
|
||||
* [poll](http://godoc.org/gotest.tools/poll) -
|
||||
test asynchronous code by polling until a desired state is reached
|
||||
* [skip](http://godoc.org/gotest.tools/skip) -
|
||||
skip a test and print the source code of the condition used to skip the test
|
||||
|
||||
## Related
|
||||
|
||||
* [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - go test runner with custom output
|
||||
* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces
|
||||
* [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time`
|
|
@ -8,7 +8,7 @@ comparison fails. The one difference is that Assert() will end the test executio
|
|||
immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()),
|
||||
return the value of the comparison, then proceed with the rest of the test case.
|
||||
|
||||
Example Usage
|
||||
Example usage
|
||||
|
||||
The example below shows assert used with some common types.
|
||||
|
||||
|
@ -16,8 +16,8 @@ The example below shows assert used with some common types.
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
func TestEverything(t *testing.T) {
|
||||
|
@ -49,12 +49,20 @@ The example below shows assert used with some common types.
|
|||
|
||||
Comparisons
|
||||
|
||||
https://godoc.org/github.com/gotestyourself/gotestyourself/assert/cmp provides
|
||||
Package https://godoc.org/gotest.tools/assert/cmp provides
|
||||
many common comparisons. Additional comparisons can be written to compare
|
||||
values in other ways. See the example Assert (CustomComparison).
|
||||
|
||||
Automated migration from testify
|
||||
|
||||
gty-migrate-from-testify is a binary which can update source code which uses
|
||||
testify assertions to use the assertions provided by this package.
|
||||
|
||||
See http://bit.do/cmd-gty-migrate-from-testify.
|
||||
|
||||
|
||||
*/
|
||||
package assert
|
||||
package assert // import "gotest.tools/assert"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -62,9 +70,9 @@ import (
|
|||
"go/token"
|
||||
|
||||
gocmp "github.com/google/go-cmp/cmp"
|
||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage.
|
||||
|
@ -234,7 +242,17 @@ func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
|
|||
}
|
||||
|
||||
// Equal uses the == operator to assert two values are equal and fails the test
|
||||
// if they are not equal. This is equivalent to Assert(t, cmp.Equal(x, y)).
|
||||
// if they are not equal.
|
||||
//
|
||||
// If the comparison fails Equal will use the variable names for x and y as part
|
||||
// of the failure message to identify the actual and expected values.
|
||||
//
|
||||
// If either x or y are a multi-line string the failure message will include a
|
||||
// unified diff of the two values. If the values only differ by whitespace
|
||||
// the unified diff will be augmented by replacing whitespace characters with
|
||||
// visible characters to identify the whitespace difference.
|
||||
//
|
||||
// This is equivalent to Assert(t, cmp.Equal(x, y)).
|
||||
func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
|
@ -242,8 +260,12 @@ func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
|
|||
assert(t, t.FailNow, argsAfterT, cmp.Equal(x, y), msgAndArgs...)
|
||||
}
|
||||
|
||||
// DeepEqual uses https://github.com/google/go-cmp/cmp to assert two values
|
||||
// are equal and fails the test if they are not equal.
|
||||
// DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are
|
||||
// equal and fails the test if they are not equal.
|
||||
//
|
||||
// Package https://godoc.org/gotest.tools/assert/opt provides some additional
|
||||
// commonly used Options.
|
||||
//
|
||||
// This is equivalent to Assert(t, cmp.DeepEqual(x, y)).
|
||||
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
|
@ -276,7 +298,7 @@ func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interf
|
|||
//
|
||||
// Expected can be one of:
|
||||
// a func(error) bool which returns true if the error is the expected type,
|
||||
// an instance of a struct of the expected type,
|
||||
// an instance of (or a pointer to) a struct of the expected type,
|
||||
// a pointer to an interface the error is expected to implement,
|
||||
// a reflect.Type of the expected struct or interface.
|
||||
//
|
|
@ -1,5 +1,5 @@
|
|||
/*Package cmp provides Comparisons for Assert and Check*/
|
||||
package cmp
|
||||
package cmp // import "gotest.tools/assert/cmp"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
"gotest.tools/internal/format"
|
||||
)
|
||||
|
||||
// Comparison is a function which compares values and returns ResultSuccess if
|
||||
|
@ -15,10 +15,12 @@ import (
|
|||
// Result will contain a message about why it failed.
|
||||
type Comparison func() Result
|
||||
|
||||
// DeepEqual compares two values using https://godoc.org/github.com/google/go-cmp/cmp
|
||||
// DeepEqual compares two values using google/go-cmp (http://bit.do/go-cmp)
|
||||
// and succeeds if the values are equal.
|
||||
//
|
||||
// The comparison can be customized using comparison Options.
|
||||
// Package https://godoc.org/gotest.tools/assert/opt provides some additional
|
||||
// commonly used Options.
|
||||
func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
||||
return func() (result Result) {
|
||||
defer func() {
|
||||
|
@ -27,7 +29,10 @@ func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
|||
}
|
||||
}()
|
||||
diff := cmp.Diff(x, y, opts...)
|
||||
return toResult(diff == "", "\n"+diff)
|
||||
if diff == "" {
|
||||
return ResultSuccess
|
||||
}
|
||||
return multiLineDiffResult(diff)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,14 +58,15 @@ func toResult(success bool, msg string) Result {
|
|||
return ResultFailure(msg)
|
||||
}
|
||||
|
||||
// Equal succeeds if x == y.
|
||||
// Equal succeeds if x == y. See assert.Equal for full documentation.
|
||||
func Equal(x, y interface{}) Comparison {
|
||||
return func() Result {
|
||||
switch {
|
||||
case x == y:
|
||||
return ResultSuccess
|
||||
case isMultiLineStringCompare(x, y):
|
||||
return multiLineStringDiffResult(x.(string), y.(string))
|
||||
diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
|
||||
return multiLineDiffResult(diff)
|
||||
}
|
||||
return ResultFailureTemplate(`
|
||||
{{- .Data.x}} (
|
||||
|
@ -86,15 +92,7 @@ func isMultiLineStringCompare(x, y interface{}) bool {
|
|||
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
|
||||
}
|
||||
|
||||
func multiLineStringDiffResult(x, y string) Result {
|
||||
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(x),
|
||||
B: difflib.SplitLines(y),
|
||||
Context: 3,
|
||||
})
|
||||
if err != nil {
|
||||
return ResultFailure(fmt.Sprintf("failed to diff: %s", err))
|
||||
}
|
||||
func multiLineDiffResult(diff string) Result {
|
||||
return ResultFailureTemplate(`
|
||||
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
|
||||
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}}
|
||||
|
@ -242,7 +240,7 @@ func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison {
|
|||
//
|
||||
// Expected can be one of:
|
||||
// a func(error) bool which returns true if the error is the expected type,
|
||||
// an instance of a struct of the expected type,
|
||||
// an instance of (or a pointer to) a struct of the expected type,
|
||||
// a pointer to an interface the error is expected to implement,
|
||||
// a reflect.Type of the expected struct or interface.
|
||||
func ErrorType(err error, expected interface{}) Comparison {
|
||||
|
@ -261,7 +259,7 @@ func ErrorType(err error, expected interface{}) Comparison {
|
|||
|
||||
expectedType := reflect.TypeOf(expected)
|
||||
switch {
|
||||
case expectedType.Kind() == reflect.Struct:
|
||||
case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType):
|
||||
return cmpErrorTypeEqualType(err, expectedType)
|
||||
case isPtrToInterface(expectedType):
|
||||
return cmpErrorTypeImplementsType(err, expectedType.Elem())
|
||||
|
@ -308,3 +306,7 @@ func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result {
|
|||
func isPtrToInterface(typ reflect.Type) bool {
|
||||
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
func isPtrToStruct(typ reflect.Type) bool {
|
||||
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
"go/ast"
|
||||
"text/template"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
// Result of a Comparison.
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"go/ast"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
func runComparison(
|
|
@ -1,13 +1,14 @@
|
|||
/*Package env provides functions to test code that read environment variables
|
||||
or the current working directory.
|
||||
*/
|
||||
package env
|
||||
package env // import "gotest.tools/env"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/x/subtest"
|
||||
)
|
||||
|
||||
type helperT interface {
|
||||
|
@ -23,7 +24,7 @@ func Patch(t assert.TestingT, key, value string) func() {
|
|||
}
|
||||
oldValue, ok := os.LookupEnv(key)
|
||||
assert.NilError(t, os.Setenv(key, value))
|
||||
return func() {
|
||||
cleanup := func() {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
@ -33,6 +34,10 @@ func Patch(t assert.TestingT, key, value string) func() {
|
|||
}
|
||||
assert.NilError(t, os.Setenv(key, oldValue))
|
||||
}
|
||||
if tc, ok := t.(subtest.TestContext); ok {
|
||||
tc.AddCleanup(cleanup)
|
||||
}
|
||||
return cleanup
|
||||
}
|
||||
|
||||
// PatchAll sets the environment to env, and returns a function which will
|
||||
|
@ -47,7 +52,7 @@ func PatchAll(t assert.TestingT, env map[string]string) func() {
|
|||
for key, value := range env {
|
||||
assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value)
|
||||
}
|
||||
return func() {
|
||||
cleanup := func() {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
@ -56,6 +61,10 @@ func PatchAll(t assert.TestingT, env map[string]string) func() {
|
|||
assert.NilError(t, os.Setenv(key, oldVal), "setenv %s=%s", key, oldVal)
|
||||
}
|
||||
}
|
||||
if tc, ok := t.(subtest.TestContext); ok {
|
||||
tc.AddCleanup(cleanup)
|
||||
}
|
||||
return cleanup
|
||||
}
|
||||
|
||||
// ToMap takes a list of strings in the format returned by os.Environ() and
|
||||
|
@ -89,10 +98,14 @@ func ChangeWorkingDir(t assert.TestingT, dir string) func() {
|
|||
cwd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, os.Chdir(dir))
|
||||
return func() {
|
||||
cleanup := func() {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert.NilError(t, os.Chdir(cwd))
|
||||
}
|
||||
if tc, ok := t.(subtest.TestContext); ok {
|
||||
tc.AddCleanup(cleanup)
|
||||
}
|
||||
return cleanup
|
||||
}
|
|
@ -1,17 +1,20 @@
|
|||
/*Package fs provides tools for creating and working with temporary files and
|
||||
directories.
|
||||
/*Package fs provides tools for creating temporary files, and testing the
|
||||
contents and structure of a directory.
|
||||
*/
|
||||
package fs
|
||||
package fs // import "gotest.tools/fs"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/x/subtest"
|
||||
)
|
||||
|
||||
// Path objects return their filesystem path. Both File and Dir implement Path.
|
||||
// Path objects return their filesystem path. Path may be implemented by a
|
||||
// real filesystem object (such as File and Dir) or by a type which updates
|
||||
// entries in a Manifest.
|
||||
type Path interface {
|
||||
Path() string
|
||||
Remove()
|
||||
|
@ -45,6 +48,9 @@ func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
|
|||
for _, op := range ops {
|
||||
assert.NilError(t, op(file))
|
||||
}
|
||||
if tc, ok := t.(subtest.TestContext); ok {
|
||||
tc.AddCleanup(file.Remove)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
|
@ -77,6 +83,9 @@ func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
|
|||
for _, op := range ops {
|
||||
assert.NilError(t, op(dir))
|
||||
}
|
||||
if tc, ok := t.(subtest.TestContext); ok {
|
||||
tc.AddCleanup(dir.Remove)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
// Manifest stores the expected structure and properties of files and directories
|
||||
// in a filesystem.
|
||||
type Manifest struct {
|
||||
root *directory
|
||||
}
|
||||
|
||||
type resource struct {
|
||||
mode os.FileMode
|
||||
uid uint32
|
||||
gid uint32
|
||||
}
|
||||
|
||||
type file struct {
|
||||
resource
|
||||
content io.ReadCloser
|
||||
}
|
||||
|
||||
func (f *file) Type() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
type symlink struct {
|
||||
resource
|
||||
target string
|
||||
}
|
||||
|
||||
func (f *symlink) Type() string {
|
||||
return "symlink"
|
||||
}
|
||||
|
||||
type directory struct {
|
||||
resource
|
||||
items map[string]dirEntry
|
||||
}
|
||||
|
||||
func (f *directory) Type() string {
|
||||
return "directory"
|
||||
}
|
||||
|
||||
type dirEntry interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
// ManifestFromDir creates a Manifest by reading the directory at path. The
|
||||
// manifest stores the structure and properties of files in the directory.
|
||||
// ManifestFromDir can be used with Equal to compare two directories.
|
||||
func ManifestFromDir(t assert.TestingT, path string) Manifest {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
||||
manifest, err := manifestFromDir(path)
|
||||
assert.NilError(t, err)
|
||||
return manifest
|
||||
}
|
||||
|
||||
func manifestFromDir(path string) (Manifest, error) {
|
||||
info, err := os.Stat(path)
|
||||
switch {
|
||||
case err != nil:
|
||||
return Manifest{}, err
|
||||
case !info.IsDir():
|
||||
return Manifest{}, errors.Errorf("path %s must be a directory", path)
|
||||
}
|
||||
|
||||
directory, err := newDirectory(path, info)
|
||||
return Manifest{root: directory}, err
|
||||
}
|
||||
|
||||
func newDirectory(path string, info os.FileInfo) (*directory, error) {
|
||||
items := make(map[string]dirEntry)
|
||||
children, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, child := range children {
|
||||
fullPath := filepath.Join(path, child.Name())
|
||||
items[child.Name()], err = getTypedResource(fullPath, child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &directory{
|
||||
resource: newResourceFromInfo(info),
|
||||
items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTypedResource(path string, info os.FileInfo) (dirEntry, error) {
|
||||
switch {
|
||||
case info.IsDir():
|
||||
return newDirectory(path, info)
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
return newSymlink(path, info)
|
||||
// TODO: devices, pipes?
|
||||
default:
|
||||
return newFile(path, info)
|
||||
}
|
||||
}
|
||||
|
||||
func newSymlink(path string, info os.FileInfo) (*symlink, error) {
|
||||
target, err := os.Readlink(path)
|
||||
return &symlink{
|
||||
resource: newResourceFromInfo(info),
|
||||
target: target,
|
||||
}, err
|
||||
}
|
||||
|
||||
func newFile(path string, info os.FileInfo) (*file, error) {
|
||||
// TODO: defer file opening to reduce number of open FDs?
|
||||
readCloser, err := os.Open(path)
|
||||
return &file{
|
||||
resource: newResourceFromInfo(info),
|
||||
content: readCloser,
|
||||
}, err
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRootDirMode = os.ModeDir | 0700
|
||||
defaultSymlinkMode = os.ModeSymlink | 0777
|
||||
)
|
||||
|
||||
func newResourceFromInfo(info os.FileInfo) resource {
|
||||
statT := info.Sys().(*syscall.Stat_t)
|
||||
return resource{
|
||||
mode: info.Mode(),
|
||||
uid: statT.Uid,
|
||||
gid: statT.Gid,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *filePath) SetMode(mode os.FileMode) {
|
||||
p.file.mode = mode
|
||||
}
|
||||
|
||||
func (p *directoryPath) SetMode(mode os.FileMode) {
|
||||
p.directory.mode = mode | os.ModeDir
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
defaultRootDirMode = os.ModeDir | 0777
|
||||
defaultSymlinkMode = os.ModeSymlink | 0666
|
||||
)
|
||||
|
||||
func newResourceFromInfo(info os.FileInfo) resource {
|
||||
return resource{mode: info.Mode()}
|
||||
}
|
||||
|
||||
func (p *filePath) SetMode(mode os.FileMode) {
|
||||
bits := mode & 0600
|
||||
p.file.mode = bits + bits/010 + bits/0100
|
||||
}
|
||||
|
||||
// TODO: is mode ignored on windows?
|
||||
func (p *directoryPath) SetMode(mode os.FileMode) {
|
||||
p.directory.mode = defaultRootDirMode
|
||||
}
|
|
@ -1,32 +1,73 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PathOp is a function which accepts a Path to perform some operation
|
||||
const defaultFileMode = 0644
|
||||
|
||||
// PathOp is a function which accepts a Path and performs an operation on that
|
||||
// path. When called with real filesystem objects (File or Dir) a PathOp modifies
|
||||
// the filesystem at the path. When used with a Manifest object a PathOp updates
|
||||
// the manifest to expect a value.
|
||||
type PathOp func(path Path) error
|
||||
|
||||
type manifestResource interface {
|
||||
SetMode(mode os.FileMode)
|
||||
SetUID(uid uint32)
|
||||
SetGID(gid uint32)
|
||||
}
|
||||
|
||||
type manifestFile interface {
|
||||
manifestResource
|
||||
SetContent(content io.ReadCloser)
|
||||
}
|
||||
|
||||
type manifestDirectory interface {
|
||||
manifestResource
|
||||
AddSymlink(path, target string) error
|
||||
AddFile(path string, ops ...PathOp) error
|
||||
AddDirectory(path string, ops ...PathOp) error
|
||||
}
|
||||
|
||||
// WithContent writes content to a file at Path
|
||||
func WithContent(content string) PathOp {
|
||||
return func(path Path) error {
|
||||
return ioutil.WriteFile(path.Path(), []byte(content), 0644)
|
||||
if m, ok := path.(manifestFile); ok {
|
||||
m.SetContent(ioutil.NopCloser(strings.NewReader(content)))
|
||||
return nil
|
||||
}
|
||||
return ioutil.WriteFile(path.Path(), []byte(content), defaultFileMode)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBytes write bytes to a file at Path
|
||||
func WithBytes(raw []byte) PathOp {
|
||||
return func(path Path) error {
|
||||
return ioutil.WriteFile(path.Path(), raw, 0644)
|
||||
if m, ok := path.(manifestFile); ok {
|
||||
m.SetContent(ioutil.NopCloser(bytes.NewReader(raw)))
|
||||
return nil
|
||||
}
|
||||
return ioutil.WriteFile(path.Path(), raw, defaultFileMode)
|
||||
}
|
||||
}
|
||||
|
||||
// AsUser changes ownership of the file system object at Path
|
||||
func AsUser(uid, gid int) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestResource); ok {
|
||||
m.SetUID(uint32(uid))
|
||||
m.SetGID(uint32(gid))
|
||||
return nil
|
||||
}
|
||||
return os.Chown(path.Path(), uid, gid)
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +75,11 @@ func AsUser(uid, gid int) PathOp {
|
|||
// WithFile creates a file in the directory at path with content
|
||||
func WithFile(filename, content string, ops ...PathOp) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
ops = append([]PathOp{WithContent(content), WithMode(defaultFileMode)}, ops...)
|
||||
return m.AddFile(filename, ops...)
|
||||
}
|
||||
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||
if err := createFile(fullpath, content); err != nil {
|
||||
return err
|
||||
|
@ -43,12 +89,22 @@ func WithFile(filename, content string, ops ...PathOp) PathOp {
|
|||
}
|
||||
|
||||
func createFile(fullpath string, content string) error {
|
||||
return ioutil.WriteFile(fullpath, []byte(content), 0644)
|
||||
return ioutil.WriteFile(fullpath, []byte(content), defaultFileMode)
|
||||
}
|
||||
|
||||
// WithFiles creates all the files in the directory at path with their content
|
||||
func WithFiles(files map[string]string) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
for filename, content := range files {
|
||||
// TODO: remove duplication with WithFile
|
||||
if err := m.AddFile(filename, WithContent(content), WithMode(defaultFileMode)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for filename, content := range files {
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||
if err := createFile(fullpath, content); err != nil {
|
||||
|
@ -62,6 +118,9 @@ func WithFiles(files map[string]string) PathOp {
|
|||
// FromDir copies the directory tree from the source path into the new Dir
|
||||
func FromDir(source string) PathOp {
|
||||
return func(path Path) error {
|
||||
if _, ok := path.(manifestDirectory); ok {
|
||||
return errors.New("use manifest.FromDir")
|
||||
}
|
||||
return copyDirectory(source, path.Path())
|
||||
}
|
||||
}
|
||||
|
@ -69,9 +128,15 @@ func FromDir(source string) PathOp {
|
|||
// WithDir creates a subdirectory in the directory at path. Additional PathOp
|
||||
// can be used to modify the subdirectory
|
||||
func WithDir(name string, ops ...PathOp) PathOp {
|
||||
const defaultMode = 0755
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
ops = append([]PathOp{WithMode(defaultMode)}, ops...)
|
||||
return m.AddDirectory(name, ops...)
|
||||
}
|
||||
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(name))
|
||||
err := os.MkdirAll(fullpath, 0755)
|
||||
err := os.MkdirAll(fullpath, defaultMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -91,6 +156,10 @@ func applyPathOps(path Path, ops []PathOp) error {
|
|||
// WithMode sets the file mode on the directory or file at path
|
||||
func WithMode(mode os.FileMode) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestResource); ok {
|
||||
m.SetMode(mode)
|
||||
return nil
|
||||
}
|
||||
return os.Chmod(path.Path(), mode)
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +181,7 @@ func copyDirectory(source, dest string) error {
|
|||
}
|
||||
continue
|
||||
}
|
||||
// TODO: handle symlinks
|
||||
if err := copyFile(sourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -134,6 +204,9 @@ func copyFile(source, dest string) error {
|
|||
// the other functions in this package.
|
||||
func WithSymlink(path, target string) PathOp {
|
||||
return func(root Path) error {
|
||||
if v, ok := root.(manifestDirectory); ok {
|
||||
return v.AddSymlink(path, target)
|
||||
}
|
||||
return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
|
||||
}
|
||||
}
|
||||
|
@ -145,6 +218,9 @@ func WithSymlink(path, target string) PathOp {
|
|||
// the other functions in this package.
|
||||
func WithHardlink(path, target string) PathOp {
|
||||
return func(root Path) error {
|
||||
if _, ok := root.(manifestDirectory); ok {
|
||||
return errors.New("WithHardlink yet implemented for manifests")
|
||||
}
|
||||
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
|
||||
}
|
||||
}
|
||||
|
@ -153,6 +229,9 @@ func WithHardlink(path, target string) PathOp {
|
|||
// at path.
|
||||
func WithTimestamps(atime, mtime time.Time) PathOp {
|
||||
return func(root Path) error {
|
||||
if _, ok := root.(manifestDirectory); ok {
|
||||
return errors.New("WithTimestamp yet implemented for manifests")
|
||||
}
|
||||
return os.Chtimes(root.Path(), atime, mtime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
// resourcePath is an adaptor for resources so they can be used as a Path
|
||||
// with PathOps.
|
||||
type resourcePath struct{}
|
||||
|
||||
func (p *resourcePath) Path() string {
|
||||
return "manifest: not a filesystem path"
|
||||
}
|
||||
|
||||
func (p *resourcePath) Remove() {}
|
||||
|
||||
type filePath struct {
|
||||
resourcePath
|
||||
file *file
|
||||
}
|
||||
|
||||
func (p *filePath) SetContent(content io.ReadCloser) {
|
||||
p.file.content = content
|
||||
}
|
||||
|
||||
func (p *filePath) SetUID(uid uint32) {
|
||||
p.file.uid = uid
|
||||
}
|
||||
|
||||
func (p *filePath) SetGID(gid uint32) {
|
||||
p.file.gid = gid
|
||||
}
|
||||
|
||||
type directoryPath struct {
|
||||
resourcePath
|
||||
directory *directory
|
||||
}
|
||||
|
||||
func (p *directoryPath) SetUID(uid uint32) {
|
||||
p.directory.uid = uid
|
||||
}
|
||||
|
||||
func (p *directoryPath) SetGID(gid uint32) {
|
||||
p.directory.gid = gid
|
||||
}
|
||||
|
||||
func (p *directoryPath) AddSymlink(path, target string) error {
|
||||
p.directory.items[path] = &symlink{
|
||||
resource: newResource(defaultSymlinkMode),
|
||||
target: target,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *directoryPath) AddFile(path string, ops ...PathOp) error {
|
||||
newFile := &file{resource: newResource(0)}
|
||||
p.directory.items[path] = newFile
|
||||
exp := &filePath{file: newFile}
|
||||
return applyPathOps(exp, ops)
|
||||
}
|
||||
|
||||
func (p *directoryPath) AddDirectory(path string, ops ...PathOp) error {
|
||||
newDir := newDirectoryWithDefaults()
|
||||
p.directory.items[path] = newDir
|
||||
exp := &directoryPath{directory: newDir}
|
||||
return applyPathOps(exp, ops)
|
||||
}
|
||||
|
||||
// Expected returns a Manifest with a directory structured created by ops. The
|
||||
// PathOp operations are applied to the manifest as expectations of the
|
||||
// filesystem structure and properties.
|
||||
func Expected(t assert.TestingT, ops ...PathOp) Manifest {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
||||
newDir := newDirectoryWithDefaults()
|
||||
e := &directoryPath{directory: newDir}
|
||||
assert.NilError(t, applyPathOps(e, ops))
|
||||
return Manifest{root: newDir}
|
||||
}
|
||||
|
||||
func newDirectoryWithDefaults() *directory {
|
||||
return &directory{
|
||||
resource: newResource(defaultRootDirMode),
|
||||
items: make(map[string]dirEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func newResource(mode os.FileMode) resource {
|
||||
return resource{
|
||||
mode: mode,
|
||||
uid: currentUID(),
|
||||
gid: currentGID(),
|
||||
}
|
||||
}
|
||||
|
||||
func currentUID() uint32 {
|
||||
return normalizeID(os.Getuid())
|
||||
}
|
||||
|
||||
func currentGID() uint32 {
|
||||
return normalizeID(os.Getgid())
|
||||
}
|
||||
|
||||
func normalizeID(id int) uint32 {
|
||||
// ids will be -1 on windows
|
||||
if id < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint32(id)
|
||||
}
|
||||
|
||||
var anyFileContent = ioutil.NopCloser(bytes.NewReader(nil))
|
||||
|
||||
// MatchAnyFileContent is a PathOp that updates a Manifest so that the file
|
||||
// at path may contain any content.
|
||||
func MatchAnyFileContent(path Path) error {
|
||||
if m, ok := path.(*filePath); ok {
|
||||
m.SetContent(anyFileContent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const anyFile = "*"
|
||||
|
||||
// MatchExtraFiles is a PathOp that updates a Manifest to allow a directory
|
||||
// to contain unspecified files.
|
||||
func MatchExtraFiles(path Path) error {
|
||||
if m, ok := path.(*directoryPath); ok {
|
||||
m.AddFile(anyFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// anyFileMode is represented by uint32_max
|
||||
const anyFileMode os.FileMode = 4294967295
|
||||
|
||||
// MatchAnyFileMode is a PathOp that updates a Manifest so that the resource at path
|
||||
// will match any file mode.
|
||||
func MatchAnyFileMode(path Path) error {
|
||||
if m, ok := path.(manifestResource); ok {
|
||||
m.SetMode(anyFileMode)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
)
|
||||
|
||||
// Equal compares a directory to the expected structured described by a manifest
|
||||
// and returns success if they match. If they do not match the failure message
|
||||
// will contain all the differences between the directory structure and the
|
||||
// expected structure defined by the Manifest.
|
||||
//
|
||||
// Equal is a cmp.Comparison which can be used with assert.Assert().
|
||||
func Equal(path string, expected Manifest) cmp.Comparison {
|
||||
return func() cmp.Result {
|
||||
actual, err := manifestFromDir(path)
|
||||
if err != nil {
|
||||
return cmp.ResultFromError(err)
|
||||
}
|
||||
failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root)
|
||||
if len(failures) == 0 {
|
||||
return cmp.ResultSuccess
|
||||
}
|
||||
msg := fmt.Sprintf("directory %s does not match expected:\n", path)
|
||||
return cmp.ResultFailure(msg + formatFailures(failures))
|
||||
}
|
||||
}
|
||||
|
||||
type failure struct {
|
||||
path string
|
||||
problems []problem
|
||||
}
|
||||
|
||||
type problem string
|
||||
|
||||
func notEqual(property string, x, y interface{}) problem {
|
||||
return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y))
|
||||
}
|
||||
|
||||
func errProblem(reason string, err error) problem {
|
||||
return problem(fmt.Sprintf("%s: %s", reason, err))
|
||||
}
|
||||
|
||||
func existenceProblem(filename, reason string, args ...interface{}) problem {
|
||||
return problem(filename + ": " + fmt.Sprintf(reason, args...))
|
||||
}
|
||||
|
||||
func eqResource(x, y resource) []problem {
|
||||
var p []problem
|
||||
if x.uid != y.uid {
|
||||
p = append(p, notEqual("uid", x.uid, y.uid))
|
||||
}
|
||||
if x.gid != y.gid {
|
||||
p = append(p, notEqual("gid", x.gid, y.gid))
|
||||
}
|
||||
if x.mode != anyFileMode && x.mode != y.mode {
|
||||
p = append(p, notEqual("mode", x.mode, y.mode))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func eqFile(x, y *file) []problem {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
|
||||
switch {
|
||||
case x.content == nil:
|
||||
p = append(p, existenceProblem("content", "expected content is nil"))
|
||||
return p
|
||||
case x.content == anyFileContent:
|
||||
return p
|
||||
case y.content == nil:
|
||||
p = append(p, existenceProblem("content", "actual content is nil"))
|
||||
return p
|
||||
}
|
||||
|
||||
xContent, xErr := ioutil.ReadAll(x.content)
|
||||
defer x.content.Close()
|
||||
yContent, yErr := ioutil.ReadAll(y.content)
|
||||
defer y.content.Close()
|
||||
|
||||
if xErr != nil {
|
||||
p = append(p, errProblem("failed to read expected content", xErr))
|
||||
}
|
||||
if yErr != nil {
|
||||
p = append(p, errProblem("failed to read actual content", xErr))
|
||||
}
|
||||
if xErr != nil || yErr != nil {
|
||||
return p
|
||||
}
|
||||
|
||||
if !bytes.Equal(xContent, yContent) {
|
||||
p = append(p, diffContent(xContent, yContent))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func diffContent(x, y []byte) problem {
|
||||
diff := format.UnifiedDiff(format.DiffConfig{
|
||||
A: string(x),
|
||||
B: string(y),
|
||||
From: "expected",
|
||||
To: "actual",
|
||||
})
|
||||
// Remove the trailing newline in the diff. A trailing newline is always
|
||||
// added to a problem by formatFailures.
|
||||
diff = strings.TrimSuffix(diff, "\n")
|
||||
return problem("content:\n" + indent(diff, " "))
|
||||
}
|
||||
|
||||
func indent(s, prefix string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
lines := strings.SplitAfter(s, "\n")
|
||||
for _, line := range lines {
|
||||
buf.WriteString(prefix + line)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func eqSymlink(x, y *symlink) []problem {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
if x.target != y.target {
|
||||
p = append(p, notEqual("target", x.target, y.target))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func eqDirectory(path string, x, y *directory) []failure {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
var f []failure
|
||||
|
||||
for _, name := range sortedKeys(x.items) {
|
||||
if name == anyFile {
|
||||
continue
|
||||
}
|
||||
xEntry := x.items[name]
|
||||
yEntry, ok := y.items[name]
|
||||
if !ok {
|
||||
p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type()))
|
||||
continue
|
||||
}
|
||||
|
||||
if xEntry.Type() != yEntry.Type() {
|
||||
p = append(p, notEqual(name, xEntry.Type(), yEntry.Type()))
|
||||
continue
|
||||
}
|
||||
|
||||
f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...)
|
||||
}
|
||||
|
||||
if _, ok := x.items[anyFile]; !ok {
|
||||
for _, name := range sortedKeys(y.items) {
|
||||
if _, ok := x.items[name]; !ok {
|
||||
yEntry := y.items[name]
|
||||
p = append(p, existenceProblem(name, "unexpected %s", yEntry.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(p) > 0 {
|
||||
f = append(f, failure{path: path, problems: p})
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func sortedKeys(items map[string]dirEntry) []string {
|
||||
var keys []string
|
||||
for key := range items {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// eqEntry assumes x and y to be the same type
|
||||
func eqEntry(path string, x, y dirEntry) []failure {
|
||||
resp := func(problems []problem) []failure {
|
||||
if len(problems) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []failure{{path: path, problems: problems}}
|
||||
}
|
||||
|
||||
switch typed := x.(type) {
|
||||
case *file:
|
||||
return resp(eqFile(typed, y.(*file)))
|
||||
case *symlink:
|
||||
return resp(eqSymlink(typed, y.(*symlink)))
|
||||
case *directory:
|
||||
return eqDirectory(path, typed, y.(*directory))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatFailures(failures []failure) string {
|
||||
sort.Slice(failures, func(i, j int) bool {
|
||||
return failures[i].path < failures[j].path
|
||||
})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
for _, failure := range failures {
|
||||
buf.WriteString(failure.path + "\n")
|
||||
for _, problem := range failure.problems {
|
||||
buf.WriteString(" " + string(problem) + "\n")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Golden files are files in the ./testdata/ subdirectory of the package under test.
|
||||
*/
|
||||
package golden
|
||||
package golden // import "gotest.tools/golden"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -11,9 +11,9 @@ import (
|
|||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
)
|
||||
|
||||
var flagUpdate = flag.Bool("test.update-golden", false, "update golden file")
|
||||
|
@ -62,16 +62,11 @@ func exactBytes(in []byte) []byte {
|
|||
// to the golden file.
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
// This is equivalent to assert.Check(t, String(actual, filename))
|
||||
//
|
||||
// Deprecated: In a future version this function will change to use assert.Assert
|
||||
// instead of assert.Check to be consistent with other assert functions.
|
||||
// Use assert.Check(t, String(actual, filename) if you want to preserve the
|
||||
// current behaviour.
|
||||
func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool {
|
||||
func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
return assert.Check(t, String(actual, filename), msgAndArgs...)
|
||||
assert.Assert(t, String(actual, filename), msgAndArgs...)
|
||||
}
|
||||
|
||||
// String compares actual to the contents of filename and returns success
|
||||
|
@ -90,16 +85,12 @@ func String(actual string, filename string) cmp.Comparison {
|
|||
if result != nil {
|
||||
return result
|
||||
}
|
||||
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(string(expected)),
|
||||
B: difflib.SplitLines(string(actualBytes)),
|
||||
FromFile: "expected",
|
||||
ToFile: "actual",
|
||||
Context: 3,
|
||||
diff := format.UnifiedDiff(format.DiffConfig{
|
||||
A: string(expected),
|
||||
B: string(actualBytes),
|
||||
From: "expected",
|
||||
To: "actual",
|
||||
})
|
||||
if err != nil {
|
||||
return cmp.ResultFromError(err)
|
||||
}
|
||||
return cmp.ResultFailure("\n" + diff)
|
||||
}
|
||||
}
|
||||
|
@ -109,21 +100,16 @@ func String(actual string, filename string) cmp.Comparison {
|
|||
// written to the golden file.
|
||||
// Returns whether the assertion was successful (true) or not (false)
|
||||
// This is equivalent to assert.Check(t, Bytes(actual, filename))
|
||||
//
|
||||
// Deprecated: In a future version this function will change to use assert.Assert
|
||||
// instead of assert.Check to be consistent with other assert functions.
|
||||
// Use assert.Check(t, Bytes(actual, filename) if you want to preserve the
|
||||
// current behaviour.
|
||||
func AssertBytes(
|
||||
t assert.TestingT,
|
||||
actual []byte,
|
||||
filename string,
|
||||
msgAndArgs ...interface{},
|
||||
) bool {
|
||||
) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
return assert.Check(t, Bytes(actual, filename), msgAndArgs...)
|
||||
assert.Assert(t, Bytes(actual, filename), msgAndArgs...)
|
||||
}
|
||||
|
||||
// Bytes compares actual to the contents of filename and returns success
|
|
@ -1,6 +1,6 @@
|
|||
/*Package icmd executes binaries and provides convenient assertions for testing the results.
|
||||
*/
|
||||
package icmd
|
||||
package icmd // import "gotest.tools/icmd"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -11,8 +11,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/assert"
|
||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
type helperT interface {
|
||||
|
@ -228,6 +228,7 @@ func StartCmd(cmd Cmd) *Result {
|
|||
return result
|
||||
}
|
||||
|
||||
// TODO: support exec.CommandContext
|
||||
func buildCmd(cmd Cmd) *Result {
|
||||
var execCmd *exec.Cmd
|
||||
switch len(cmd.Command) {
|
|
@ -1,27 +1,10 @@
|
|||
// Package difflib is a partial port of Python difflib module.
|
||||
//
|
||||
// It provides tools to compare sequences of strings and generate textual diffs.
|
||||
//
|
||||
// The following class and functions have been ported:
|
||||
//
|
||||
// - SequenceMatcher
|
||||
//
|
||||
// - unified_diff
|
||||
//
|
||||
// - context_diff
|
||||
//
|
||||
// Getting unified diffs was the main goal of the port. Keep in mind this code
|
||||
// is mostly suitable to output text differences in a human friendly way, there
|
||||
// are no guarantees generated diffs are consumable by patch(1).
|
||||
package difflib
|
||||
/* Package difflib is a partial port of Python difflib module.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
Original source: https://github.com/pmezard/go-difflib
|
||||
|
||||
This file is trimmed to only the parts used by this repository.
|
||||
*/
|
||||
package difflib // import "gotest.tools/internal/difflib"
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
|
@ -37,13 +20,6 @@ func max(a, b int) int {
|
|||
return b
|
||||
}
|
||||
|
||||
func calculateRatio(matches, length int) float64 {
|
||||
if length > 0 {
|
||||
return 2.0 * float64(matches) / float64(length)
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
A int
|
||||
B int
|
||||
|
@ -103,14 +79,6 @@ func NewMatcher(a, b []string) *SequenceMatcher {
|
|||
return &m
|
||||
}
|
||||
|
||||
func NewMatcherWithJunk(a, b []string, autoJunk bool,
|
||||
isJunk func(string) bool) *SequenceMatcher {
|
||||
|
||||
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
|
||||
m.SetSeqs(a, b)
|
||||
return &m
|
||||
}
|
||||
|
||||
// Set two sequences to be compared.
|
||||
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
||||
m.SetSeq1(a)
|
||||
|
@ -450,323 +418,3 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
|
|||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// Return a measure of the sequences' similarity (float in [0,1]).
|
||||
//
|
||||
// Where T is the total number of elements in both sequences, and
|
||||
// M is the number of matches, this is 2.0*M / T.
|
||||
// Note that this is 1 if the sequences are identical, and 0 if
|
||||
// they have nothing in common.
|
||||
//
|
||||
// .Ratio() is expensive to compute if you haven't already computed
|
||||
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
|
||||
// want to try .QuickRatio() or .RealQuickRation() first to get an
|
||||
// upper bound.
|
||||
func (m *SequenceMatcher) Ratio() float64 {
|
||||
matches := 0
|
||||
for _, m := range m.GetMatchingBlocks() {
|
||||
matches += m.Size
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() relatively quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute.
|
||||
func (m *SequenceMatcher) QuickRatio() float64 {
|
||||
// viewing a and b as multisets, set matches to the cardinality
|
||||
// of their intersection; this counts the number of matches
|
||||
// without regard to order, so is clearly an upper bound
|
||||
if m.fullBCount == nil {
|
||||
m.fullBCount = map[string]int{}
|
||||
for _, s := range m.b {
|
||||
m.fullBCount[s] = m.fullBCount[s] + 1
|
||||
}
|
||||
}
|
||||
|
||||
// avail[x] is the number of times x appears in 'b' less the
|
||||
// number of times we've seen it in 'a' so far ... kinda
|
||||
avail := map[string]int{}
|
||||
matches := 0
|
||||
for _, s := range m.a {
|
||||
n, ok := avail[s]
|
||||
if !ok {
|
||||
n = m.fullBCount[s]
|
||||
}
|
||||
avail[s] = n - 1
|
||||
if n > 0 {
|
||||
matches += 1
|
||||
}
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() very quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute than either .Ratio() or .QuickRatio().
|
||||
func (m *SequenceMatcher) RealQuickRatio() float64 {
|
||||
la, lb := len(m.a), len(m.b)
|
||||
return calculateRatio(min(la, lb), la+lb)
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format
|
||||
func formatRangeUnified(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
beginning := start + 1 // lines start numbering with one
|
||||
length := stop - start
|
||||
if length == 1 {
|
||||
return fmt.Sprintf("%d", beginning)
|
||||
}
|
||||
if length == 0 {
|
||||
beginning -= 1 // empty ranges begin at line just before the range
|
||||
}
|
||||
return fmt.Sprintf("%d,%d", beginning, length)
|
||||
}
|
||||
|
||||
// Unified diff parameters
|
||||
type UnifiedDiff struct {
|
||||
A []string // First sequence lines
|
||||
FromFile string // First file name
|
||||
FromDate string // First file time
|
||||
B []string // Second sequence lines
|
||||
ToFile string // Second file name
|
||||
ToDate string // Second file time
|
||||
Eol string // Headers end of line, defaults to LF
|
||||
Context int // Number of context lines
|
||||
}
|
||||
|
||||
// Compare two sequences of lines; generate the delta as a unified diff.
|
||||
//
|
||||
// Unified diffs are a compact way of showing line changes and a few
|
||||
// lines of context. The number of context lines is set by 'n' which
|
||||
// defaults to three.
|
||||
//
|
||||
// By default, the diff control lines (those with ---, +++, or @@) are
|
||||
// created with a trailing newline. This is helpful so that inputs
|
||||
// created from file.readlines() result in diffs that are suitable for
|
||||
// file.writelines() since both the inputs and outputs have trailing
|
||||
// newlines.
|
||||
//
|
||||
// For inputs that do not have trailing newlines, set the lineterm
|
||||
// argument to "" so that the output will be uniformly newline free.
|
||||
//
|
||||
// The unidiff format normally has a header for filenames and modification
|
||||
// times. Any or all of these may be specified using strings for
|
||||
// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
|
||||
// The modification times are normally expressed in the ISO 8601 format.
|
||||
func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
|
||||
buf := bufio.NewWriter(writer)
|
||||
defer buf.Flush()
|
||||
wf := func(format string, args ...interface{}) error {
|
||||
_, err := buf.WriteString(fmt.Sprintf(format, args...))
|
||||
return err
|
||||
}
|
||||
ws := func(s string) error {
|
||||
_, err := buf.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(diff.Eol) == 0 {
|
||||
diff.Eol = "\n"
|
||||
}
|
||||
|
||||
started := false
|
||||
m := NewMatcher(diff.A, diff.B)
|
||||
for _, g := range m.GetGroupedOpCodes(diff.Context) {
|
||||
if !started {
|
||||
started = true
|
||||
fromDate := ""
|
||||
if len(diff.FromDate) > 0 {
|
||||
fromDate = "\t" + diff.FromDate
|
||||
}
|
||||
toDate := ""
|
||||
if len(diff.ToDate) > 0 {
|
||||
toDate = "\t" + diff.ToDate
|
||||
}
|
||||
if diff.FromFile != "" || diff.ToFile != "" {
|
||||
err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
first, last := g[0], g[len(g)-1]
|
||||
range1 := formatRangeUnified(first.I1, last.I2)
|
||||
range2 := formatRangeUnified(first.J1, last.J2)
|
||||
if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range g {
|
||||
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
|
||||
if c.Tag == 'e' {
|
||||
for _, line := range diff.A[i1:i2] {
|
||||
if err := ws(" " + line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c.Tag == 'r' || c.Tag == 'd' {
|
||||
for _, line := range diff.A[i1:i2] {
|
||||
if err := ws("-" + line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Tag == 'r' || c.Tag == 'i' {
|
||||
for _, line := range diff.B[j1:j2] {
|
||||
if err := ws("+" + line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Like WriteUnifiedDiff but returns the diff a string.
|
||||
func GetUnifiedDiffString(diff UnifiedDiff) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
err := WriteUnifiedDiff(w, diff)
|
||||
return string(w.Bytes()), err
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format.
|
||||
func formatRangeContext(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
beginning := start + 1 // lines start numbering with one
|
||||
length := stop - start
|
||||
if length == 0 {
|
||||
beginning -= 1 // empty ranges begin at line just before the range
|
||||
}
|
||||
if length <= 1 {
|
||||
return fmt.Sprintf("%d", beginning)
|
||||
}
|
||||
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
|
||||
}
|
||||
|
||||
type ContextDiff UnifiedDiff
|
||||
|
||||
// Compare two sequences of lines; generate the delta as a context diff.
|
||||
//
|
||||
// Context diffs are a compact way of showing line changes and a few
|
||||
// lines of context. The number of context lines is set by diff.Context
|
||||
// which defaults to three.
|
||||
//
|
||||
// By default, the diff control lines (those with *** or ---) are
|
||||
// created with a trailing newline.
|
||||
//
|
||||
// For inputs that do not have trailing newlines, set the diff.Eol
|
||||
// argument to "" so that the output will be uniformly newline free.
|
||||
//
|
||||
// The context diff format normally has a header for filenames and
|
||||
// modification times. Any or all of these may be specified using
|
||||
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
|
||||
// The modification times are normally expressed in the ISO 8601 format.
|
||||
// If not specified, the strings default to blanks.
|
||||
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
|
||||
buf := bufio.NewWriter(writer)
|
||||
defer buf.Flush()
|
||||
var diffErr error
|
||||
wf := func(format string, args ...interface{}) {
|
||||
_, err := buf.WriteString(fmt.Sprintf(format, args...))
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
ws := func(s string) {
|
||||
_, err := buf.WriteString(s)
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if len(diff.Eol) == 0 {
|
||||
diff.Eol = "\n"
|
||||
}
|
||||
|
||||
prefix := map[byte]string{
|
||||
'i': "+ ",
|
||||
'd': "- ",
|
||||
'r': "! ",
|
||||
'e': " ",
|
||||
}
|
||||
|
||||
started := false
|
||||
m := NewMatcher(diff.A, diff.B)
|
||||
for _, g := range m.GetGroupedOpCodes(diff.Context) {
|
||||
if !started {
|
||||
started = true
|
||||
fromDate := ""
|
||||
if len(diff.FromDate) > 0 {
|
||||
fromDate = "\t" + diff.FromDate
|
||||
}
|
||||
toDate := ""
|
||||
if len(diff.ToDate) > 0 {
|
||||
toDate = "\t" + diff.ToDate
|
||||
}
|
||||
if diff.FromFile != "" || diff.ToFile != "" {
|
||||
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
|
||||
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
|
||||
}
|
||||
}
|
||||
|
||||
first, last := g[0], g[len(g)-1]
|
||||
ws("***************" + diff.Eol)
|
||||
|
||||
range1 := formatRangeContext(first.I1, last.I2)
|
||||
wf("*** %s ****%s", range1, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'd' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'i' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.A[cc.I1:cc.I2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
range2 := formatRangeContext(first.J1, last.J2)
|
||||
wf("--- %s ----%s", range2, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'i' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'd' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.B[cc.J1:cc.J2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffErr
|
||||
}
|
||||
|
||||
// Like WriteContextDiff but returns the diff a string.
|
||||
func GetContextDiffString(diff ContextDiff) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
err := WriteContextDiff(w, diff)
|
||||
return string(w.Bytes()), err
|
||||
}
|
||||
|
||||
// Split a string on "\n" while preserving them. The output can be used
|
||||
// as input for UnifiedDiff and ContextDiff structures.
|
||||
func SplitLines(s string) []string {
|
||||
lines := strings.SplitAfter(s, "\n")
|
||||
lines[len(lines)-1] += "\n"
|
||||
return lines
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"gotest.tools/internal/difflib"
|
||||
)
|
||||
|
||||
const (
|
||||
contextLines = 2
|
||||
)
|
||||
|
||||
// DiffConfig for a unified diff
|
||||
type DiffConfig struct {
|
||||
A string
|
||||
B string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
// UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better
|
||||
// support for showing the whitespace differences.
|
||||
func UnifiedDiff(conf DiffConfig) string {
|
||||
a := strings.SplitAfter(conf.A, "\n")
|
||||
b := strings.SplitAfter(conf.B, "\n")
|
||||
groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines)
|
||||
if len(groups) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writeFormat := func(format string, args ...interface{}) {
|
||||
buf.WriteString(fmt.Sprintf(format, args...))
|
||||
}
|
||||
writeLine := func(prefix string, s string) {
|
||||
buf.WriteString(prefix + s)
|
||||
}
|
||||
if hasWhitespaceDiffLines(groups, a, b) {
|
||||
writeLine = visibleWhitespaceLine(writeLine)
|
||||
}
|
||||
formatHeader(writeFormat, conf)
|
||||
for _, group := range groups {
|
||||
formatRangeLine(writeFormat, group)
|
||||
for _, opCode := range group {
|
||||
in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2]
|
||||
switch opCode.Tag {
|
||||
case 'e':
|
||||
formatLines(writeLine, " ", in)
|
||||
case 'r':
|
||||
formatLines(writeLine, "-", in)
|
||||
formatLines(writeLine, "+", out)
|
||||
case 'd':
|
||||
formatLines(writeLine, "-", in)
|
||||
case 'i':
|
||||
formatLines(writeLine, "+", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// hasWhitespaceDiffLines returns true if any diff groups is only different
|
||||
// because of whitespace characters.
|
||||
func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool {
|
||||
for _, group := range groups {
|
||||
in, out := new(bytes.Buffer), new(bytes.Buffer)
|
||||
for _, opCode := range group {
|
||||
if opCode.Tag == 'e' {
|
||||
continue
|
||||
}
|
||||
for _, line := range a[opCode.I1:opCode.I2] {
|
||||
in.WriteString(line)
|
||||
}
|
||||
for _, line := range b[opCode.J1:opCode.J2] {
|
||||
out.WriteString(line)
|
||||
}
|
||||
}
|
||||
if removeWhitespace(in.String()) == removeWhitespace(out.String()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func removeWhitespace(s string) string {
|
||||
var result []rune
|
||||
for _, r := range s {
|
||||
if !unicode.IsSpace(r) {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func visibleWhitespaceLine(ws func(string, string)) func(string, string) {
|
||||
mapToVisibleSpace := func(r rune) rune {
|
||||
switch r {
|
||||
case '\n':
|
||||
case ' ':
|
||||
return '·'
|
||||
case '\t':
|
||||
return '▷'
|
||||
case '\v':
|
||||
return '▽'
|
||||
case '\r':
|
||||
return '↵'
|
||||
case '\f':
|
||||
return '↓'
|
||||
default:
|
||||
if unicode.IsSpace(r) {
|
||||
return '<27>'
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
return func(prefix, s string) {
|
||||
ws(prefix, strings.Map(mapToVisibleSpace, s))
|
||||
}
|
||||
}
|
||||
|
||||
func formatHeader(wf func(string, ...interface{}), conf DiffConfig) {
|
||||
if conf.From != "" || conf.To != "" {
|
||||
wf("--- %s\n", conf.From)
|
||||
wf("+++ %s\n", conf.To)
|
||||
}
|
||||
}
|
||||
|
||||
func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) {
|
||||
first, last := group[0], group[len(group)-1]
|
||||
range1 := formatRangeUnified(first.I1, last.I2)
|
||||
range2 := formatRangeUnified(first.J1, last.J2)
|
||||
wf("@@ -%s +%s @@\n", range1, range2)
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format
|
||||
func formatRangeUnified(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
beginning := start + 1 // lines start numbering with one
|
||||
length := stop - start
|
||||
if length == 1 {
|
||||
return fmt.Sprintf("%d", beginning)
|
||||
}
|
||||
if length == 0 {
|
||||
beginning-- // empty ranges begin at line just before the range
|
||||
}
|
||||
return fmt.Sprintf("%d,%d", beginning, length)
|
||||
}
|
||||
|
||||
func formatLines(writeLine func(string, string), prefix string, lines []string) {
|
||||
for _, line := range lines {
|
||||
writeLine(prefix, line)
|
||||
}
|
||||
// Add a newline if the last line is missing one so that the diff displays
|
||||
// properly.
|
||||
if !strings.HasSuffix(lines[len(lines)-1], "\n") {
|
||||
writeLine("", "\n")
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package format
|
||||
package format // import "gotest.tools/internal/format"
|
||||
|
||||
import "fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package source
|
||||
package source // import "gotest.tools/internal/source"
|
||||
|
||||
import (
|
||||
"bytes"
|
|
@ -1,6 +1,6 @@
|
|||
/*Package poll provides tools for testing asynchronous code.
|
||||
*/
|
||||
package poll
|
||||
package poll // import "gotest.tools/poll"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -25,15 +25,15 @@ type helperT interface {
|
|||
|
||||
// Settings are used to configure the behaviour of WaitOn
|
||||
type Settings struct {
|
||||
// Timeout is the maximum time to wait for the condition. Defaults to 10s
|
||||
// Timeout is the maximum time to wait for the condition. Defaults to 10s.
|
||||
Timeout time.Duration
|
||||
// Delay is the time to sleep between checking the condition. Detaults to
|
||||
// 1ms
|
||||
// Delay is the time to sleep between checking the condition. Defaults to
|
||||
// 100ms.
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
func defaultConfig() *Settings {
|
||||
return &Settings{Timeout: 10 * time.Second, Delay: time.Millisecond}
|
||||
return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond}
|
||||
}
|
||||
|
||||
// SettingOp is a function which accepts and modifies Settings
|
|
@ -1,6 +1,7 @@
|
|||
/*Package skip provides functions for skipping based on a condition.
|
||||
/*Package skip provides functions for skipping a test and printing the source code
|
||||
of the condition used to skip the test.
|
||||
*/
|
||||
package skip
|
||||
package skip // import "gotest.tools/skip"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -9,8 +10,8 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
||||
"gotest.tools/internal/format"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
type skipT interface {
|
||||
|
@ -25,9 +26,10 @@ type helperT interface {
|
|||
// BoolOrCheckFunc can be a bool or func() bool, other types will panic
|
||||
type BoolOrCheckFunc interface{}
|
||||
|
||||
// If skips the test if the check function returns true. The skip message will
|
||||
// contain the name of the check function. Extra message text can be passed as a
|
||||
// format string with args
|
||||
// If the condition expression evaluates to true, or the condition function returns
|
||||
// true, skip the test.
|
||||
// The skip message will contain the source code of the expression.
|
||||
// Extra message text can be passed as a format string with args
|
||||
func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
|
@ -49,18 +51,6 @@ func getFunctionName(function func() bool) string {
|
|||
return strings.SplitN(path.Base(funcPath), ".", 2)[1]
|
||||
}
|
||||
|
||||
// IfCondition skips the test if the condition is true. The skip message will
|
||||
// contain the source of the expression passed as the condition. Extra message
|
||||
// text can be passed as a format string with args.
|
||||
//
|
||||
// Deprecated: Use If() which now accepts bool arguments
|
||||
func IfCondition(t skipT, condition bool, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
ifCondition(t, condition, msgAndArgs...)
|
||||
}
|
||||
|
||||
func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
|
@ -0,0 +1,81 @@
|
|||
/*Package subtest provides a TestContext to subtests which handles cleanup, and
|
||||
provides a testing.TB, and context.Context.
|
||||
|
||||
This package was inspired by github.com/frankban/quicktest.
|
||||
*/
|
||||
package subtest // import "gotest.tools/x/subtest"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testcase struct {
|
||||
testing.TB
|
||||
ctx context.Context
|
||||
cleanupFuncs []cleanupFunc
|
||||
}
|
||||
|
||||
type cleanupFunc func()
|
||||
|
||||
func (tc *testcase) Ctx() context.Context {
|
||||
if tc.ctx == nil {
|
||||
var cancel func()
|
||||
tc.ctx, cancel = context.WithCancel(context.Background())
|
||||
tc.AddCleanup(cancel)
|
||||
}
|
||||
return tc.ctx
|
||||
}
|
||||
|
||||
// Cleanup runs all cleanup functions. Functions are run in the opposite order
|
||||
// in which they were added. Cleanup is called automatically before Run exits.
|
||||
func (tc *testcase) Cleanup() {
|
||||
for _, f := range tc.cleanupFuncs {
|
||||
// Defer all cleanup functions so they all run even if one calls
|
||||
// t.FailNow() or panics. Deferring them also runs them in reverse order.
|
||||
defer f()
|
||||
}
|
||||
tc.cleanupFuncs = nil
|
||||
}
|
||||
|
||||
func (tc *testcase) AddCleanup(f func()) {
|
||||
tc.cleanupFuncs = append(tc.cleanupFuncs, f)
|
||||
}
|
||||
|
||||
func (tc *testcase) Parallel() {
|
||||
tp, ok := tc.TB.(parallel)
|
||||
if !ok {
|
||||
panic("Parallel called with a testing.B")
|
||||
}
|
||||
tp.Parallel()
|
||||
}
|
||||
|
||||
type parallel interface {
|
||||
Parallel()
|
||||
}
|
||||
|
||||
// Run a subtest. When subtest exits, every cleanup function added with
|
||||
// TestContext.AddCleanup will be run.
|
||||
func Run(t *testing.T, name string, subtest func(t TestContext)) bool {
|
||||
return t.Run(name, func(t *testing.T) {
|
||||
tc := &testcase{TB: t}
|
||||
defer tc.Cleanup()
|
||||
subtest(tc)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContext provides a testing.TB and a context.Context for a test case.
|
||||
type TestContext interface {
|
||||
testing.TB
|
||||
// AddCleanup function which will be run when before Run returns.
|
||||
AddCleanup(f func())
|
||||
// Ctx returns a context for the test case. Multiple calls from the same subtest
|
||||
// will return the same context. The context is cancelled when Run
|
||||
// returns.
|
||||
Ctx() context.Context
|
||||
// Parallel calls t.Parallel on the testing.TB. Panics if testing.TB does
|
||||
// not implement Parallel.
|
||||
Parallel()
|
||||
}
|
||||
|
||||
var _ TestContext = &testcase{}
|
Loading…
Reference in New Issue