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/googleapis/gnostic e4f56557df6250e1945ee6854f181ce4e1c2c646
|
||||||
github.com/gorilla/context v1.1
|
github.com/gorilla/context v1.1
|
||||||
github.com/gorilla/mux 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/jsonpointer 46af16f9f7b149af66e5d1bd010e3574dc06de98
|
||||||
github.com/go-openapi/jsonreference 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
|
github.com/go-openapi/jsonreference 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
|
||||||
github.com/go-openapi/spec 6aced65f8501fe1217321abf0749d354824ba2ff
|
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/opencontainers/runc 4fc53a81fb7c994640722ac585fa9ca548971871
|
||||||
github.com/peterbourgon/diskv 5f041e8faa004a95c88a202771f4cc3e991971e6
|
github.com/peterbourgon/diskv 5f041e8faa004a95c88a202771f4cc3e991971e6
|
||||||
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
||||||
github.com/pmezard/go-difflib v1.0.0
|
|
||||||
github.com/prometheus/client_golang 52437c81da6b127a9925d17eb3a382a2e5fd395e
|
github.com/prometheus/client_golang 52437c81da6b127a9925d17eb3a382a2e5fd395e
|
||||||
github.com/prometheus/client_model fa8ad6fec33561be4280a8f0514318c79d7f6cb6
|
github.com/prometheus/client_model fa8ad6fec33561be4280a8f0514318c79d7f6cb6
|
||||||
github.com/prometheus/common ebdfc6da46522d58825777cf1f90490a5b1ef1d8
|
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()),
|
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.
|
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.
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/assert"
|
"gotest.tools/assert"
|
||||||
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
is "gotest.tools/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEverything(t *testing.T) {
|
func TestEverything(t *testing.T) {
|
||||||
|
@ -49,12 +49,20 @@ The example below shows assert used with some common types.
|
||||||
|
|
||||||
Comparisons
|
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
|
many common comparisons. Additional comparisons can be written to compare
|
||||||
values in other ways. See the example Assert (CustomComparison).
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -62,9 +70,9 @@ import (
|
||||||
"go/token"
|
"go/token"
|
||||||
|
|
||||||
gocmp "github.com/google/go-cmp/cmp"
|
gocmp "github.com/google/go-cmp/cmp"
|
||||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
"gotest.tools/assert/cmp"
|
||||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
"gotest.tools/internal/format"
|
||||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
"gotest.tools/internal/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage.
|
// 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
|
// 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{}) {
|
func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
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...)
|
assert(t, t.FailNow, argsAfterT, cmp.Equal(x, y), msgAndArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepEqual uses https://github.com/google/go-cmp/cmp to assert two values
|
// DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are
|
||||||
// are equal and fails the test if they are not equal.
|
// 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)).
|
// This is equivalent to Assert(t, cmp.DeepEqual(x, y)).
|
||||||
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
|
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
|
||||||
if ht, ok := t.(helperT); ok {
|
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:
|
// Expected can be one of:
|
||||||
// a func(error) bool which returns true if the error is the expected type,
|
// 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 pointer to an interface the error is expected to implement,
|
||||||
// a reflect.Type of the expected struct or interface.
|
// a reflect.Type of the expected struct or interface.
|
||||||
//
|
//
|
|
@ -1,5 +1,5 @@
|
||||||
/*Package cmp provides Comparisons for Assert and Check*/
|
/*Package cmp provides Comparisons for Assert and Check*/
|
||||||
package cmp
|
package cmp // import "gotest.tools/assert/cmp"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"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
|
// 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.
|
// Result will contain a message about why it failed.
|
||||||
type Comparison func() Result
|
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.
|
// and succeeds if the values are equal.
|
||||||
//
|
//
|
||||||
// The comparison can be customized using comparison Options.
|
// 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 {
|
func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
||||||
return func() (result Result) {
|
return func() (result Result) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -27,7 +29,10 @@ func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
diff := cmp.Diff(x, y, opts...)
|
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)
|
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 {
|
func Equal(x, y interface{}) Comparison {
|
||||||
return func() Result {
|
return func() Result {
|
||||||
switch {
|
switch {
|
||||||
case x == y:
|
case x == y:
|
||||||
return ResultSuccess
|
return ResultSuccess
|
||||||
case isMultiLineStringCompare(x, y):
|
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(`
|
return ResultFailureTemplate(`
|
||||||
{{- .Data.x}} (
|
{{- .Data.x}} (
|
||||||
|
@ -86,15 +92,7 @@ func isMultiLineStringCompare(x, y interface{}) bool {
|
||||||
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
|
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func multiLineStringDiffResult(x, y string) Result {
|
func multiLineDiffResult(diff 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))
|
|
||||||
}
|
|
||||||
return ResultFailureTemplate(`
|
return ResultFailureTemplate(`
|
||||||
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
|
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
|
||||||
+++ {{ with callArg 1 }}{{ 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:
|
// Expected can be one of:
|
||||||
// a func(error) bool which returns true if the error is the expected type,
|
// 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 pointer to an interface the error is expected to implement,
|
||||||
// a reflect.Type of the expected struct or interface.
|
// a reflect.Type of the expected struct or interface.
|
||||||
func ErrorType(err error, expected interface{}) Comparison {
|
func ErrorType(err error, expected interface{}) Comparison {
|
||||||
|
@ -261,7 +259,7 @@ func ErrorType(err error, expected interface{}) Comparison {
|
||||||
|
|
||||||
expectedType := reflect.TypeOf(expected)
|
expectedType := reflect.TypeOf(expected)
|
||||||
switch {
|
switch {
|
||||||
case expectedType.Kind() == reflect.Struct:
|
case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType):
|
||||||
return cmpErrorTypeEqualType(err, expectedType)
|
return cmpErrorTypeEqualType(err, expectedType)
|
||||||
case isPtrToInterface(expectedType):
|
case isPtrToInterface(expectedType):
|
||||||
return cmpErrorTypeImplementsType(err, expectedType.Elem())
|
return cmpErrorTypeImplementsType(err, expectedType.Elem())
|
||||||
|
@ -308,3 +306,7 @@ func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result {
|
||||||
func isPtrToInterface(typ reflect.Type) bool {
|
func isPtrToInterface(typ reflect.Type) bool {
|
||||||
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface
|
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"
|
"go/ast"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
"gotest.tools/internal/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Result of a Comparison.
|
// Result of a Comparison.
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
"gotest.tools/assert/cmp"
|
||||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
"gotest.tools/internal/format"
|
||||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
"gotest.tools/internal/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runComparison(
|
func runComparison(
|
|
@ -1,13 +1,14 @@
|
||||||
/*Package env provides functions to test code that read environment variables
|
/*Package env provides functions to test code that read environment variables
|
||||||
or the current working directory.
|
or the current working directory.
|
||||||
*/
|
*/
|
||||||
package env
|
package env // import "gotest.tools/env"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/assert"
|
"gotest.tools/assert"
|
||||||
|
"gotest.tools/x/subtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type helperT interface {
|
type helperT interface {
|
||||||
|
@ -23,7 +24,7 @@ func Patch(t assert.TestingT, key, value string) func() {
|
||||||
}
|
}
|
||||||
oldValue, ok := os.LookupEnv(key)
|
oldValue, ok := os.LookupEnv(key)
|
||||||
assert.NilError(t, os.Setenv(key, value))
|
assert.NilError(t, os.Setenv(key, value))
|
||||||
return func() {
|
cleanup := func() {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
ht.Helper()
|
||||||
}
|
}
|
||||||
|
@ -33,6 +34,10 @@ func Patch(t assert.TestingT, key, value string) func() {
|
||||||
}
|
}
|
||||||
assert.NilError(t, os.Setenv(key, oldValue))
|
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
|
// 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 {
|
for key, value := range env {
|
||||||
assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value)
|
assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value)
|
||||||
}
|
}
|
||||||
return func() {
|
cleanup := func() {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
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)
|
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
|
// 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()
|
cwd, err := os.Getwd()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.NilError(t, os.Chdir(dir))
|
assert.NilError(t, os.Chdir(dir))
|
||||||
return func() {
|
cleanup := func() {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
ht.Helper()
|
||||||
}
|
}
|
||||||
assert.NilError(t, os.Chdir(cwd))
|
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
|
/*Package fs provides tools for creating temporary files, and testing the
|
||||||
directories.
|
contents and structure of a directory.
|
||||||
*/
|
*/
|
||||||
package fs
|
package fs // import "gotest.tools/fs"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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 {
|
type Path interface {
|
||||||
Path() string
|
Path() string
|
||||||
Remove()
|
Remove()
|
||||||
|
@ -45,6 +48,9 @@ func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
assert.NilError(t, op(file))
|
assert.NilError(t, op(file))
|
||||||
}
|
}
|
||||||
|
if tc, ok := t.(subtest.TestContext); ok {
|
||||||
|
tc.AddCleanup(file.Remove)
|
||||||
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +83,9 @@ func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
assert.NilError(t, op(dir))
|
assert.NilError(t, op(dir))
|
||||||
}
|
}
|
||||||
|
if tc, ok := t.(subtest.TestContext); ok {
|
||||||
|
tc.AddCleanup(dir.Remove)
|
||||||
|
}
|
||||||
return dir
|
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
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"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 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
|
// WithContent writes content to a file at Path
|
||||||
func WithContent(content string) PathOp {
|
func WithContent(content string) PathOp {
|
||||||
return func(path Path) error {
|
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
|
// WithBytes write bytes to a file at Path
|
||||||
func WithBytes(raw []byte) PathOp {
|
func WithBytes(raw []byte) PathOp {
|
||||||
return func(path Path) error {
|
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
|
// AsUser changes ownership of the file system object at Path
|
||||||
func AsUser(uid, gid int) PathOp {
|
func AsUser(uid, gid int) PathOp {
|
||||||
return func(path Path) error {
|
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)
|
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
|
// WithFile creates a file in the directory at path with content
|
||||||
func WithFile(filename, content string, ops ...PathOp) PathOp {
|
func WithFile(filename, content string, ops ...PathOp) PathOp {
|
||||||
return func(path Path) error {
|
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))
|
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||||
if err := createFile(fullpath, content); err != nil {
|
if err := createFile(fullpath, content); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -43,12 +89,22 @@ func WithFile(filename, content string, ops ...PathOp) PathOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(fullpath string, content string) error {
|
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
|
// WithFiles creates all the files in the directory at path with their content
|
||||||
func WithFiles(files map[string]string) PathOp {
|
func WithFiles(files map[string]string) PathOp {
|
||||||
return func(path Path) error {
|
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 {
|
for filename, content := range files {
|
||||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||||
if err := createFile(fullpath, content); err != nil {
|
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
|
// FromDir copies the directory tree from the source path into the new Dir
|
||||||
func FromDir(source string) PathOp {
|
func FromDir(source string) PathOp {
|
||||||
return func(path Path) error {
|
return func(path Path) error {
|
||||||
|
if _, ok := path.(manifestDirectory); ok {
|
||||||
|
return errors.New("use manifest.FromDir")
|
||||||
|
}
|
||||||
return copyDirectory(source, path.Path())
|
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
|
// WithDir creates a subdirectory in the directory at path. Additional PathOp
|
||||||
// can be used to modify the subdirectory
|
// can be used to modify the subdirectory
|
||||||
func WithDir(name string, ops ...PathOp) PathOp {
|
func WithDir(name string, ops ...PathOp) PathOp {
|
||||||
|
const defaultMode = 0755
|
||||||
return func(path Path) error {
|
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))
|
fullpath := filepath.Join(path.Path(), filepath.FromSlash(name))
|
||||||
err := os.MkdirAll(fullpath, 0755)
|
err := os.MkdirAll(fullpath, defaultMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// WithMode sets the file mode on the directory or file at path
|
||||||
func WithMode(mode os.FileMode) PathOp {
|
func WithMode(mode os.FileMode) PathOp {
|
||||||
return func(path Path) error {
|
return func(path Path) error {
|
||||||
|
if m, ok := path.(manifestResource); ok {
|
||||||
|
m.SetMode(mode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return os.Chmod(path.Path(), mode)
|
return os.Chmod(path.Path(), mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +181,7 @@ func copyDirectory(source, dest string) error {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// TODO: handle symlinks
|
||||||
if err := copyFile(sourcePath, destPath); err != nil {
|
if err := copyFile(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -134,6 +204,9 @@ func copyFile(source, dest string) error {
|
||||||
// the other functions in this package.
|
// the other functions in this package.
|
||||||
func WithSymlink(path, target string) PathOp {
|
func WithSymlink(path, target string) PathOp {
|
||||||
return func(root Path) error {
|
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))
|
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.
|
// the other functions in this package.
|
||||||
func WithHardlink(path, target string) PathOp {
|
func WithHardlink(path, target string) PathOp {
|
||||||
return func(root Path) error {
|
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))
|
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.
|
// at path.
|
||||||
func WithTimestamps(atime, mtime time.Time) PathOp {
|
func WithTimestamps(atime, mtime time.Time) PathOp {
|
||||||
return func(root Path) error {
|
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)
|
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.
|
Golden files are files in the ./testdata/ subdirectory of the package under test.
|
||||||
*/
|
*/
|
||||||
package golden
|
package golden // import "gotest.tools/golden"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -11,9 +11,9 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/assert"
|
"gotest.tools/assert"
|
||||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
"gotest.tools/assert/cmp"
|
||||||
"github.com/pmezard/go-difflib/difflib"
|
"gotest.tools/internal/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
var flagUpdate = flag.Bool("test.update-golden", false, "update golden file")
|
var flagUpdate = flag.Bool("test.update-golden", false, "update golden file")
|
||||||
|
@ -62,16 +62,11 @@ func exactBytes(in []byte) []byte {
|
||||||
// to the golden file.
|
// to the golden file.
|
||||||
// Returns whether the assertion was successful (true) or not (false).
|
// Returns whether the assertion was successful (true) or not (false).
|
||||||
// This is equivalent to assert.Check(t, String(actual, filename))
|
// This is equivalent to assert.Check(t, String(actual, filename))
|
||||||
//
|
func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) {
|
||||||
// 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 {
|
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
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
|
// 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 {
|
if result != nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
|
diff := format.UnifiedDiff(format.DiffConfig{
|
||||||
A: difflib.SplitLines(string(expected)),
|
A: string(expected),
|
||||||
B: difflib.SplitLines(string(actualBytes)),
|
B: string(actualBytes),
|
||||||
FromFile: "expected",
|
From: "expected",
|
||||||
ToFile: "actual",
|
To: "actual",
|
||||||
Context: 3,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return cmp.ResultFromError(err)
|
|
||||||
}
|
|
||||||
return cmp.ResultFailure("\n" + diff)
|
return cmp.ResultFailure("\n" + diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,21 +100,16 @@ func String(actual string, filename string) cmp.Comparison {
|
||||||
// written to the golden file.
|
// written to the golden file.
|
||||||
// Returns whether the assertion was successful (true) or not (false)
|
// Returns whether the assertion was successful (true) or not (false)
|
||||||
// This is equivalent to assert.Check(t, Bytes(actual, filename))
|
// 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(
|
func AssertBytes(
|
||||||
t assert.TestingT,
|
t assert.TestingT,
|
||||||
actual []byte,
|
actual []byte,
|
||||||
filename string,
|
filename string,
|
||||||
msgAndArgs ...interface{},
|
msgAndArgs ...interface{},
|
||||||
) bool {
|
) {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
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
|
// 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 executes binaries and provides convenient assertions for testing the results.
|
||||||
*/
|
*/
|
||||||
package icmd
|
package icmd // import "gotest.tools/icmd"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -11,8 +11,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/assert"
|
"gotest.tools/assert"
|
||||||
"github.com/gotestyourself/gotestyourself/assert/cmp"
|
"gotest.tools/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type helperT interface {
|
type helperT interface {
|
||||||
|
@ -228,6 +228,7 @@ func StartCmd(cmd Cmd) *Result {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: support exec.CommandContext
|
||||||
func buildCmd(cmd Cmd) *Result {
|
func buildCmd(cmd Cmd) *Result {
|
||||||
var execCmd *exec.Cmd
|
var execCmd *exec.Cmd
|
||||||
switch len(cmd.Command) {
|
switch len(cmd.Command) {
|
|
@ -1,27 +1,10 @@
|
||||||
// Package difflib is a partial port of Python difflib module.
|
/* 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
|
|
||||||
|
|
||||||
import (
|
Original source: https://github.com/pmezard/go-difflib
|
||||||
"bufio"
|
|
||||||
"bytes"
|
This file is trimmed to only the parts used by this repository.
|
||||||
"fmt"
|
*/
|
||||||
"io"
|
package difflib // import "gotest.tools/internal/difflib"
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func min(a, b int) int {
|
func min(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
|
@ -37,13 +20,6 @@ func max(a, b int) int {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateRatio(matches, length int) float64 {
|
|
||||||
if length > 0 {
|
|
||||||
return 2.0 * float64(matches) / float64(length)
|
|
||||||
}
|
|
||||||
return 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
type Match struct {
|
type Match struct {
|
||||||
A int
|
A int
|
||||||
B int
|
B int
|
||||||
|
@ -103,14 +79,6 @@ func NewMatcher(a, b []string) *SequenceMatcher {
|
||||||
return &m
|
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.
|
// Set two sequences to be compared.
|
||||||
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
||||||
m.SetSeq1(a)
|
m.SetSeq1(a)
|
||||||
|
@ -450,323 +418,3 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
|
||||||
}
|
}
|
||||||
return groups
|
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"
|
import "fmt"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package source // import "gotest.tools/internal/source"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -1,6 +1,6 @@
|
||||||
/*Package poll provides tools for testing asynchronous code.
|
/*Package poll provides tools for testing asynchronous code.
|
||||||
*/
|
*/
|
||||||
package poll
|
package poll // import "gotest.tools/poll"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -25,15 +25,15 @@ type helperT interface {
|
||||||
|
|
||||||
// Settings are used to configure the behaviour of WaitOn
|
// Settings are used to configure the behaviour of WaitOn
|
||||||
type Settings struct {
|
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
|
Timeout time.Duration
|
||||||
// Delay is the time to sleep between checking the condition. Detaults to
|
// Delay is the time to sleep between checking the condition. Defaults to
|
||||||
// 1ms
|
// 100ms.
|
||||||
Delay time.Duration
|
Delay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultConfig() *Settings {
|
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
|
// 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -9,8 +10,8 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gotestyourself/gotestyourself/internal/format"
|
"gotest.tools/internal/format"
|
||||||
"github.com/gotestyourself/gotestyourself/internal/source"
|
"gotest.tools/internal/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
type skipT interface {
|
type skipT interface {
|
||||||
|
@ -25,9 +26,10 @@ type helperT interface {
|
||||||
// BoolOrCheckFunc can be a bool or func() bool, other types will panic
|
// BoolOrCheckFunc can be a bool or func() bool, other types will panic
|
||||||
type BoolOrCheckFunc interface{}
|
type BoolOrCheckFunc interface{}
|
||||||
|
|
||||||
// If skips the test if the check function returns true. The skip message will
|
// If the condition expression evaluates to true, or the condition function returns
|
||||||
// contain the name of the check function. Extra message text can be passed as a
|
// true, skip the test.
|
||||||
// format string with args
|
// 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{}) {
|
func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
ht.Helper()
|
||||||
|
@ -49,18 +51,6 @@ func getFunctionName(function func() bool) string {
|
||||||
return strings.SplitN(path.Base(funcPath), ".", 2)[1]
|
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{}) {
|
func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) {
|
||||||
if ht, ok := t.(helperT); ok {
|
if ht, ok := t.(helperT); ok {
|
||||||
ht.Helper()
|
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