chore: vendor

This commit is contained in:
2024-08-04 11:06:58 +02:00
parent 2a5985e44e
commit 04aec8232f
3557 changed files with 981078 additions and 1 deletions

13
vendor/gotest.tools/v3/LICENSE vendored Normal file
View File

@ -0,0 +1,13 @@
Copyright 2018 gotest.tools authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

313
vendor/gotest.tools/v3/assert/assert.go vendored Normal file
View File

@ -0,0 +1,313 @@
/*
Package assert provides assertions for comparing expected values to actual
values in tests. When an assertion fails a helpful error message is printed.
# Example usage
All the assertions in this package use [testing.T.Helper] to mark themselves as
test helpers. This allows the testing package to print the filename and line
number of the file function that failed.
assert.NilError(t, err)
// filename_test.go:212: assertion failed: error is not nil: file not found
If any assertion is called from a helper function, make sure to call t.Helper
from the helper function so that the filename and line number remain correct.
The examples below show assert used with some common types and the failure
messages it produces. The filename and line number portion of the failure
message is omitted from these examples for brevity.
// booleans
assert.Assert(t, ok)
// assertion failed: ok is false
assert.Assert(t, !missing)
// assertion failed: missing is true
// primitives
assert.Equal(t, count, 1)
// assertion failed: 0 (count int) != 1 (int)
assert.Equal(t, msg, "the message")
// assertion failed: my message (msg string) != the message (string)
assert.Assert(t, total != 10) // use Assert for NotEqual
// assertion failed: total is 10
assert.Assert(t, count > 20, "count=%v", count)
// assertion failed: count is <= 20: count=1
// errors
assert.NilError(t, closer.Close())
// assertion failed: error is not nil: close /file: errno 11
assert.Error(t, err, "the exact error message")
// assertion failed: expected error "the exact error message", got "oops"
assert.ErrorContains(t, err, "includes this")
// assertion failed: expected error to contain "includes this", got "oops"
assert.ErrorIs(t, err, os.ErrNotExist)
// assertion failed: error is "oops", not "file does not exist" (os.ErrNotExist)
// complex types
assert.DeepEqual(t, result, myStruct{Name: "title"})
// assertion failed: ... (diff of the two structs)
assert.Assert(t, is.Len(items, 3))
// assertion failed: expected [] (length 0) to have length 3
assert.Assert(t, len(sequence) != 0) // use Assert for NotEmpty
// assertion failed: len(sequence) is 0
assert.Assert(t, is.Contains(mapping, "key"))
// assertion failed: map[other:1] does not contain key
// pointers and interface
assert.Assert(t, ref == nil)
// assertion failed: ref is not nil
assert.Assert(t, ref != nil) // use Assert for NotNil
// assertion failed: ref is nil
# Assert and Check
[Assert] and [Check] are very similar, they both accept a [cmp.Comparison], and fail
the test when the comparison fails. The one difference is that Assert uses
[testing.T.FailNow] to fail the test, which will end the test execution immediately.
Check uses [testing.T.Fail] to fail the test, which allows it to return the
result of the comparison, then proceed with the rest of the test case.
Like [testing.T.FailNow], [Assert] must be called from the goroutine running the test,
not from other goroutines created during the test. [Check] is safe to use from any
goroutine.
# Comparisons
Package [gotest.tools/v3/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 command which translates Go source code from
testify assertions to the assertions provided by this package.
See http://pkg.go.dev/gotest.tools/v3/assert/cmd/gty-migrate-from-testify.
*/
package assert // import "gotest.tools/v3/assert"
import (
gocmp "github.com/google/go-cmp/cmp"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/internal/assert"
)
// BoolOrComparison can be a bool, [cmp.Comparison], or error. See [Assert] for
// details about how this type is used.
type BoolOrComparison interface{}
// TestingT is the subset of [testing.T] (see also [testing.TB]) used by the assert package.
type TestingT interface {
FailNow()
Fail()
Log(args ...interface{})
}
type helperT interface {
Helper()
}
// Assert performs a comparison. If the comparison fails, the test is marked as
// failed, a failure message is logged, and execution is stopped immediately.
//
// The comparison argument may be one of three types:
//
// bool
// True is success. False is a failure. The failure message will contain
// the literal source code of the expression.
//
// cmp.Comparison
// Uses cmp.Result.Success() to check for success or failure.
// The comparison is responsible for producing a helpful failure message.
// http://pkg.go.dev/gotest.tools/v3/assert/cmp provides many common comparisons.
//
// error
// A nil value is considered success, and a non-nil error is a failure.
// The return value of error.Error is used as the failure message.
//
// Extra details can be added to the failure message using msgAndArgs. msgAndArgs
// may be either a single string, or a format string and args that will be
// passed to [fmt.Sprintf].
//
// Assert uses [testing.TB.FailNow] to fail the test. Like t.FailNow, Assert must be called
// from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] from other goroutines.
func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) {
t.FailNow()
}
}
// Check performs a comparison. If the comparison fails the test is marked as
// failed, a failure message is printed, and Check returns false. If the comparison
// is successful Check returns true. Check may be called from any goroutine.
//
// See [Assert] for details about the comparison arg and failure messages.
func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) {
t.Fail()
return false
}
return true
}
// NilError fails the test immediately if err is not nil, and includes err.Error
// in the failure message.
//
// NilError uses [testing.TB.FailNow] to fail the test. Like t.FailNow, NilError must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] from other goroutines.
func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, err, msgAndArgs...) {
t.FailNow()
}
}
// Equal uses the == operator to assert two values are equal and fails the test
// if they are not equal.
//
// If the comparison fails Equal will use the variable names and types of
// x and y as part of the failure message to identify the actual and expected
// values.
//
// assert.Equal(t, actual, expected)
// // main_test.go:41: assertion failed: 1 (actual int) != 21 (expected int32)
//
// 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.
//
// Equal uses [testing.T.FailNow] to fail the test. Like t.FailNow, Equal must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.Equal] from other
// goroutines.
func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.Equal(x, y), msgAndArgs...) {
t.FailNow()
}
}
// DeepEqual uses [github.com/google/go-cmp/cmp]
// to assert two values are equal and fails the test if they are not equal.
//
// Package [gotest.tools/v3/assert/opt] provides some additional
// commonly used Options.
//
// DeepEqual uses [testing.T.FailNow] to fail the test. Like t.FailNow, DeepEqual must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.DeepEqual] from other
// goroutines.
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.DeepEqual(x, y, opts...)) {
t.FailNow()
}
}
// Error fails the test if err is nil, or if err.Error is not equal to expected.
// Both err.Error and expected will be included in the failure message.
// Error performs an exact match of the error text. Use [ErrorContains] if only
// part of the error message is relevant. Use [ErrorType] or [ErrorIs] to compare
// errors by type.
//
// Error uses [testing.T.FailNow] to fail the test. Like t.FailNow, Error must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.Error] from other
// goroutines.
func Error(t TestingT, err error, expected string, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, expected), msgAndArgs...) {
t.FailNow()
}
}
// ErrorContains fails the test if err is nil, or if err.Error does not
// contain the expected substring. Both err.Error and the expected substring
// will be included in the failure message.
//
// ErrorContains uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorContains
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.ErrorContains] from other
// goroutines.
func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...) {
t.FailNow()
}
}
// ErrorType fails the test if err is nil, or err is not the expected type.
// New code should use ErrorIs instead.
//
// Expected can be one of:
//
// func(error) bool
// The function should return true if the error is the expected type.
//
// struct{} or *struct{}
// A struct or a pointer to a struct. The assertion fails if the error is
// not of the same type.
//
// *interface{}
// A pointer to an interface type. The assertion fails if err does not
// implement the interface.
//
// reflect.Type
// The assertion fails if err does not implement the reflect.Type.
//
// ErrorType uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorType
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.ErrorType] from other
// goroutines.
//
// Deprecated: Use [ErrorIs]
func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorType(err, expected), msgAndArgs...) {
t.FailNow()
}
}
// ErrorIs fails the test if err is nil, or the error does not match expected
// when compared using errors.Is. See [errors.Is] for
// accepted arguments.
//
// ErrorIs uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorIs
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use [Check] with [cmp.ErrorIs] from other
// goroutines.
func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) {
t.FailNow()
}
}

View File

@ -0,0 +1,404 @@
/*Package cmp provides Comparisons for Assert and Check*/
package cmp // import "gotest.tools/v3/assert/cmp"
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/google/go-cmp/cmp"
"gotest.tools/v3/internal/format"
)
// Comparison is a function which compares values and returns [ResultSuccess] if
// the actual value matches the expected value. If the values do not match the
// [Result] will contain a message about why it failed.
type Comparison func() Result
// DeepEqual compares two values using [github.com/google/go-cmp/cmp]
// and succeeds if the values are equal.
//
// The comparison can be customized using comparison Options.
// Package [gotest.tools/v3/assert/opt] provides some additional
// commonly used Options.
func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
return func() (result Result) {
defer func() {
if panicmsg, handled := handleCmpPanic(recover()); handled {
result = ResultFailure(panicmsg)
}
}()
diff := cmp.Diff(x, y, opts...)
if diff == "" {
return ResultSuccess
}
return multiLineDiffResult(diff, x, y)
}
}
func handleCmpPanic(r interface{}) (string, bool) {
if r == nil {
return "", false
}
panicmsg, ok := r.(string)
if !ok {
panic(r)
}
switch {
case strings.HasPrefix(panicmsg, "cannot handle unexported field"):
return panicmsg, true
}
panic(r)
}
func toResult(success bool, msg string) Result {
if success {
return ResultSuccess
}
return ResultFailure(msg)
}
// RegexOrPattern may be either a [*regexp.Regexp] or a string that is a valid
// regexp pattern.
type RegexOrPattern interface{}
// Regexp succeeds if value v matches regular expression re.
//
// Example:
//
// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str))
// r := regexp.MustCompile("^[0-9a-f]{32}$")
// assert.Assert(t, cmp.Regexp(r, str))
func Regexp(re RegexOrPattern, v string) Comparison {
match := func(re *regexp.Regexp) Result {
return toResult(
re.MatchString(v),
fmt.Sprintf("value %q does not match regexp %q", v, re.String()))
}
return func() Result {
switch regex := re.(type) {
case *regexp.Regexp:
return match(regex)
case string:
re, err := regexp.Compile(regex)
if err != nil {
return ResultFailure(err.Error())
}
return match(re)
default:
return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex))
}
}
}
// Equal succeeds if x == y. See [gotest.tools/v3/assert.Equal] for full documentation.
func Equal(x, y interface{}) Comparison {
return func() Result {
switch {
case x == y:
return ResultSuccess
case isMultiLineStringCompare(x, y):
diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
return multiLineDiffResult(diff, x, y)
}
return ResultFailureTemplate(`
{{- printf "%v" .Data.x}} (
{{- with callArg 0 }}{{ formatNode . }} {{end -}}
{{- printf "%T" .Data.x -}}
) != {{ printf "%v" .Data.y}} (
{{- with callArg 1 }}{{ formatNode . }} {{end -}}
{{- printf "%T" .Data.y -}}
)`,
map[string]interface{}{"x": x, "y": y})
}
}
func isMultiLineStringCompare(x, y interface{}) bool {
strX, ok := x.(string)
if !ok {
return false
}
strY, ok := y.(string)
if !ok {
return false
}
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
}
func multiLineDiffResult(diff string, x, y interface{}) Result {
return ResultFailureTemplate(`
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}{{end}}
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}{{end}}
{{ .Data.diff }}`,
map[string]interface{}{"diff": diff, "x": x, "y": y})
}
// Len succeeds if the sequence has the expected length.
func Len(seq interface{}, expected int) Comparison {
return func() (result Result) {
defer func() {
if e := recover(); e != nil {
result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq))
}
}()
value := reflect.ValueOf(seq)
length := value.Len()
if length == expected {
return ResultSuccess
}
msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected)
return ResultFailure(msg)
}
}
// Contains succeeds if item is in collection. Collection may be a string, map,
// slice, or array.
//
// If collection is a string, item must also be a string, and is compared using
// [strings.Contains].
// If collection is a Map, contains will succeed if item is a key in the map.
// If collection is a slice or array, item is compared to each item in the
// sequence using [reflect.DeepEqual].
func Contains(collection interface{}, item interface{}) Comparison {
return func() Result {
colValue := reflect.ValueOf(collection)
if !colValue.IsValid() {
return ResultFailure("nil does not contain items")
}
msg := fmt.Sprintf("%v does not contain %v", collection, item)
itemValue := reflect.ValueOf(item)
switch colValue.Type().Kind() {
case reflect.String:
if itemValue.Type().Kind() != reflect.String {
return ResultFailure("string may only contain strings")
}
return toResult(
strings.Contains(colValue.String(), itemValue.String()),
fmt.Sprintf("string %q does not contain %q", collection, item))
case reflect.Map:
if itemValue.Type() != colValue.Type().Key() {
return ResultFailure(fmt.Sprintf(
"%v can not contain a %v key", colValue.Type(), itemValue.Type()))
}
return toResult(colValue.MapIndex(itemValue).IsValid(), msg)
case reflect.Slice, reflect.Array:
for i := 0; i < colValue.Len(); i++ {
if reflect.DeepEqual(colValue.Index(i).Interface(), item) {
return ResultSuccess
}
}
return ResultFailure(msg)
default:
return ResultFailure(fmt.Sprintf("type %T does not contain items", collection))
}
}
}
// Panics succeeds if f() panics.
func Panics(f func()) Comparison {
return func() (result Result) {
defer func() {
if err := recover(); err != nil {
result = ResultSuccess
}
}()
f()
return ResultFailure("did not panic")
}
}
// Error succeeds if err is a non-nil error, and the error message equals the
// expected message.
func Error(err error, message string) Comparison {
return func() Result {
switch {
case err == nil:
return ResultFailure("expected an error, got nil")
case err.Error() != message:
return ResultFailure(fmt.Sprintf(
"expected error %q, got %s", message, formatErrorMessage(err)))
}
return ResultSuccess
}
}
// ErrorContains succeeds if err is a non-nil error, and the error message contains
// the expected substring.
func ErrorContains(err error, substring string) Comparison {
return func() Result {
switch {
case err == nil:
return ResultFailure("expected an error, got nil")
case !strings.Contains(err.Error(), substring):
return ResultFailure(fmt.Sprintf(
"expected error to contain %q, got %s", substring, formatErrorMessage(err)))
}
return ResultSuccess
}
}
type causer interface {
Cause() error
}
func formatErrorMessage(err error) string {
//nolint:errorlint // unwrapping is not appropriate here
if _, ok := err.(causer); ok {
return fmt.Sprintf("%q\n%+v", err, err)
}
// This error was not wrapped with github.com/pkg/errors
return fmt.Sprintf("%q", err)
}
// Nil succeeds if obj is a nil interface, pointer, or function.
//
// Use [gotest.tools/v3/assert.NilError] for comparing errors. Use Len(obj, 0) for comparing slices,
// maps, and channels.
func Nil(obj interface{}) Comparison {
msgFunc := func(value reflect.Value) string {
return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type())
}
return isNil(obj, msgFunc)
}
func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison {
return func() Result {
if obj == nil {
return ResultSuccess
}
value := reflect.ValueOf(obj)
kind := value.Type().Kind()
if kind >= reflect.Chan && kind <= reflect.Slice {
if value.IsNil() {
return ResultSuccess
}
return ResultFailure(msgFunc(value))
}
return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type()))
}
}
// ErrorType succeeds if err is not nil and is of the expected type.
//
// Expected can be one of:
//
// func(error) bool
//
// Function should return true if the error is the expected type.
//
// type struct{}, type &struct{}
//
// A struct or a pointer to a struct.
// Fails if the error is not of the same type as expected.
//
// type &interface{}
//
// A pointer to an interface type.
// Fails if err does not implement the interface.
//
// reflect.Type
//
// Fails if err does not implement the [reflect.Type].
//
// Deprecated: Use [ErrorIs]
func ErrorType(err error, expected interface{}) Comparison {
return func() Result {
switch expectedType := expected.(type) {
case func(error) bool:
return cmpErrorTypeFunc(err, expectedType)
case reflect.Type:
if expectedType.Kind() == reflect.Interface {
return cmpErrorTypeImplementsType(err, expectedType)
}
return cmpErrorTypeEqualType(err, expectedType)
case nil:
return ResultFailure("invalid type for expected: nil")
}
expectedType := reflect.TypeOf(expected)
switch {
case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType):
return cmpErrorTypeEqualType(err, expectedType)
case isPtrToInterface(expectedType):
return cmpErrorTypeImplementsType(err, expectedType.Elem())
}
return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected))
}
}
func cmpErrorTypeFunc(err error, f func(error) bool) Result {
if f(err) {
return ResultSuccess
}
actual := "nil"
if err != nil {
actual = fmt.Sprintf("%s (%T)", err, err)
}
return ResultFailureTemplate(`error is {{ .Data.actual }}
{{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`,
map[string]interface{}{"actual": actual})
}
func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result {
if err == nil {
return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
}
errValue := reflect.ValueOf(err)
if errValue.Type() == expectedType {
return ResultSuccess
}
return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
}
func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result {
if err == nil {
return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
}
errValue := reflect.ValueOf(err)
if errValue.Type().Implements(expectedType) {
return ResultSuccess
}
return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
}
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
}
var (
stdlibErrorNewType = reflect.TypeOf(errors.New(""))
stdlibFmtErrorType = reflect.TypeOf(fmt.Errorf("%w", fmt.Errorf("")))
)
// ErrorIs succeeds if errors.Is(actual, expected) returns true. See
// [errors.Is] for accepted argument values.
func ErrorIs(actual error, expected error) Comparison {
return func() Result {
if errors.Is(actual, expected) {
return ResultSuccess
}
// The type of stdlib errors is excluded because the type is not relevant
// in those cases. The type is only important when it is a user defined
// custom error type.
return ResultFailureTemplate(`error is
{{- if not .Data.a }} nil,{{ else }}
{{- printf " \"%v\"" .Data.a }}
{{- if notStdlibErrorType .Data.a }} ({{ printf "%T" .Data.a }}){{ end }},
{{- end }} not {{ printf "\"%v\"" .Data.x }} (
{{- with callArg 1 }}{{ formatNode . }}{{ end }}
{{- if notStdlibErrorType .Data.x }}{{ printf " %T" .Data.x }}{{ end }})`,
map[string]interface{}{"a": actual, "x": expected})
}
}

View File

@ -0,0 +1,110 @@
package cmp
import (
"bytes"
"fmt"
"go/ast"
"reflect"
"text/template"
"gotest.tools/v3/internal/source"
)
// A Result of a [Comparison].
type Result interface {
Success() bool
}
// StringResult is an implementation of [Result] that reports the error message
// string verbatim and does not provide any templating or formatting of the
// message.
type StringResult struct {
success bool
message string
}
// Success returns true if the comparison was successful.
func (r StringResult) Success() bool {
return r.success
}
// FailureMessage returns the message used to provide additional information
// about the failure.
func (r StringResult) FailureMessage() string {
return r.message
}
// ResultSuccess is a constant which is returned by a [Comparison] to
// indicate success.
var ResultSuccess = StringResult{success: true}
// ResultFailure returns a failed [Result] with a failure message.
func ResultFailure(message string) StringResult {
return StringResult{message: message}
}
// ResultFromError returns [ResultSuccess] if err is nil. Otherwise [ResultFailure]
// is returned with the error message as the failure message.
func ResultFromError(err error) Result {
if err == nil {
return ResultSuccess
}
return ResultFailure(err.Error())
}
type templatedResult struct {
template string
data map[string]interface{}
}
func (r templatedResult) Success() bool {
return false
}
func (r templatedResult) FailureMessage(args []ast.Expr) string {
msg, err := renderMessage(r, args)
if err != nil {
return fmt.Sprintf("failed to render failure message: %s", err)
}
return msg
}
func (r templatedResult) UpdatedExpected(stackIndex int) error {
// TODO: would be nice to have structured data instead of a map
return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"])
}
// ResultFailureTemplate returns a [Result] with a template string and data which
// can be used to format a failure message. The template may access data from .Data,
// the comparison args with the callArg function, and the formatNode function may
// be used to format the call args.
func ResultFailureTemplate(template string, data map[string]interface{}) Result {
return templatedResult{template: template, data: data}
}
func renderMessage(result templatedResult, args []ast.Expr) (string, error) {
tmpl := template.New("failure").Funcs(template.FuncMap{
"formatNode": source.FormatNode,
"callArg": func(index int) ast.Expr {
if index >= len(args) {
return nil
}
return args[index]
},
// TODO: any way to include this from ErrorIS instead of here?
"notStdlibErrorType": func(typ interface{}) bool {
r := reflect.TypeOf(typ)
return r != stdlibFmtErrorType && r != stdlibErrorNewType
},
})
var err error
tmpl, err = tmpl.Parse(result.template)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, map[string]interface{}{
"Data": result.data,
})
return buf.String(), err
}

129
vendor/gotest.tools/v3/fs/file.go vendored Normal file
View File

@ -0,0 +1,129 @@
/*
Package fs provides tools for creating temporary files, and testing the
contents and structure of a directory.
*/
package fs // import "gotest.tools/v3/fs"
import (
"os"
"path/filepath"
"runtime"
"strings"
"gotest.tools/v3/assert"
"gotest.tools/v3/internal/cleanup"
)
// 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()
}
var (
_ Path = &Dir{}
_ Path = &File{}
)
// File is a temporary file on the filesystem
type File struct {
path string
}
type helperT interface {
Helper()
}
// NewFile creates a new file in a temporary directory using prefix as part of
// the filename. The PathOps are applied to the before returning the File.
//
// When used with Go 1.14+ the file will be automatically removed when the test
// ends, unless the TEST_NOCLEANUP env var is set to true.
func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
tempfile, err := os.CreateTemp("", cleanPrefix(prefix)+"-")
assert.NilError(t, err)
file := &File{path: tempfile.Name()}
cleanup.Cleanup(t, file.Remove)
assert.NilError(t, tempfile.Close())
assert.NilError(t, applyPathOps(file, ops))
return file
}
func cleanPrefix(prefix string) string {
// windows requires both / and \ are replaced
if runtime.GOOS == "windows" {
prefix = strings.Replace(prefix, string(os.PathSeparator), "-", -1)
}
return strings.Replace(prefix, "/", "-", -1)
}
// Path returns the full path to the file
func (f *File) Path() string {
return f.path
}
// Remove the file
func (f *File) Remove() {
_ = os.Remove(f.path)
}
// Dir is a temporary directory
type Dir struct {
path string
}
// NewDir returns a new temporary directory using prefix as part of the directory
// name. The PathOps are applied before returning the Dir.
//
// When used with Go 1.14+ the directory will be automatically removed when the test
// ends, unless the TEST_NOCLEANUP env var is set to true.
func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
path, err := os.MkdirTemp("", cleanPrefix(prefix)+"-")
assert.NilError(t, err)
dir := &Dir{path: path}
cleanup.Cleanup(t, dir.Remove)
assert.NilError(t, applyPathOps(dir, ops))
return dir
}
// Path returns the full path to the directory
func (d *Dir) Path() string {
return d.path
}
// Remove the directory
func (d *Dir) Remove() {
_ = os.RemoveAll(d.path)
}
// Join returns a new path with this directory as the base of the path
func (d *Dir) Join(parts ...string) string {
return filepath.Join(append([]string{d.Path()}, parts...)...)
}
// DirFromPath returns a Dir for a path that already exists. No directory is created.
// Unlike NewDir the directory will not be removed automatically when the test exits,
// it is the callers responsibly to remove the directory.
// DirFromPath can be used with Apply to modify an existing directory.
//
// If the path does not already exist, use NewDir instead.
func DirFromPath(t assert.TestingT, path string, ops ...PathOp) *Dir {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
dir := &Dir{path: path}
assert.NilError(t, applyPathOps(dir, ops))
return dir
}

142
vendor/gotest.tools/v3/fs/manifest.go vendored Normal file
View File

@ -0,0 +1,142 @@
package fs
import (
"fmt"
"io"
"os"
"path/filepath"
"gotest.tools/v3/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
ignoreCariageReturn bool
compareContentFunc func(b []byte) CompareResult
}
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
filepathGlobs map[string]*filePath
}
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{}, fmt.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 := os.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,
filepathGlobs: make(map[string]*filePath),
}, nil
}
func getTypedResource(path string, entry os.DirEntry) (dirEntry, error) {
info, err := entry.Info()
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, err
}
return &file{
resource: newResourceFromInfo(info),
content: readCloser,
}, err
}

View File

@ -0,0 +1,38 @@
//go:build !windows
// +build !windows
package fs
import (
"os"
"runtime"
"syscall"
)
const defaultRootDirMode = os.ModeDir | 0700
var defaultSymlinkMode = os.ModeSymlink | 0777
func init() {
switch runtime.GOOS {
case "darwin":
defaultSymlinkMode = os.ModeSymlink | 0755
}
}
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
}

View File

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

278
vendor/gotest.tools/v3/fs/ops.go vendored Normal file
View File

@ -0,0 +1,278 @@
package fs
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"gotest.tools/v3/assert"
)
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 {
if m, ok := path.(manifestFile); ok {
m.SetContent(io.NopCloser(strings.NewReader(content)))
return nil
}
return os.WriteFile(path.Path(), []byte(content), defaultFileMode)
}
}
// WithBytes write bytes to a file at [Path]
func WithBytes(raw []byte) PathOp {
return func(path Path) error {
if m, ok := path.(manifestFile); ok {
m.SetContent(io.NopCloser(bytes.NewReader(raw)))
return nil
}
return os.WriteFile(path.Path(), raw, defaultFileMode)
}
}
// WithReaderContent copies the reader contents to the file at [Path]
func WithReaderContent(r io.Reader) PathOp {
return func(path Path) error {
if m, ok := path.(manifestFile); ok {
m.SetContent(io.NopCloser(r))
return nil
}
f, err := os.OpenFile(path.Path(), os.O_WRONLY, defaultFileMode)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, r)
return err
}
}
// 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)
}
}
// 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
}
return applyPathOps(&File{path: fullpath}, ops)
}
}
func createFile(fullpath string, content string) error {
return os.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 {
return err
}
}
return nil
}
}
// 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 fmt.Errorf("use manifest.FromDir")
}
return copyDirectory(source, path.Path())
}
}
// 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, defaultMode)
if err != nil {
return err
}
return applyPathOps(&Dir{path: fullpath}, ops)
}
}
// Apply the PathOps to the [File]
func Apply(t assert.TestingT, path Path, ops ...PathOp) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
assert.NilError(t, applyPathOps(path, ops))
}
func applyPathOps(path Path, ops []PathOp) error {
for _, op := range ops {
if err := op(path); err != nil {
return err
}
}
return nil
}
// 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)
}
}
func copyDirectory(source, dest string) error {
entries, err := os.ReadDir(source)
if err != nil {
return err
}
for _, entry := range entries {
sourcePath := filepath.Join(source, entry.Name())
destPath := filepath.Join(dest, entry.Name())
err = copyEntry(entry, destPath, sourcePath)
if err != nil {
return err
}
}
return nil
}
func copyEntry(entry os.DirEntry, destPath string, sourcePath string) error {
if entry.IsDir() {
if err := os.Mkdir(destPath, 0755); err != nil {
return err
}
return copyDirectory(sourcePath, destPath)
}
info, err := entry.Info()
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return copySymLink(sourcePath, destPath)
}
return copyFile(sourcePath, destPath)
}
func copySymLink(source, dest string) error {
link, err := os.Readlink(source)
if err != nil {
return err
}
return os.Symlink(link, dest)
}
func copyFile(source, dest string) error {
content, err := os.ReadFile(source)
if err != nil {
return err
}
return os.WriteFile(dest, content, 0644)
}
// WithSymlink creates a symlink in the directory which links to target.
// Target must be a path relative to the directory.
//
// Note: the argument order is the inverse of [os.Symlink] to be consistent with
// 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))
}
}
// WithHardlink creates a link in the directory which links to target.
// Target must be a path relative to the directory.
//
// Note: the argument order is the inverse of [os.Link] to be consistent with
// the other functions in this package.
func WithHardlink(path, target string) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return fmt.Errorf("WithHardlink not implemented for manifests")
}
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
}
}
// WithTimestamps sets the access and modification times of the file system object
// at path.
func WithTimestamps(atime, mtime time.Time) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return fmt.Errorf("WithTimestamp not implemented for manifests")
}
return os.Chtimes(root.Path(), atime, mtime)
}
}

198
vendor/gotest.tools/v3/fs/path.go vendored Normal file
View File

@ -0,0 +1,198 @@
package fs
import (
"bytes"
"io"
"os"
"gotest.tools/v3/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) AddGlobFiles(glob string, ops ...PathOp) error {
newFile := &file{resource: newResource(0)}
newFilePath := &filePath{file: newFile}
p.directory.filepathGlobs[glob] = newFilePath
return applyPathOps(newFilePath, 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),
filepathGlobs: make(map[string]*filePath),
}
}
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 = io.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
}
// MatchContentIgnoreCarriageReturn is a [PathOp] that ignores cariage return
// discrepancies.
func MatchContentIgnoreCarriageReturn(path Path) error {
if m, ok := path.(*filePath); ok {
m.file.ignoreCariageReturn = true
}
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 {
return m.AddFile(anyFile)
}
return nil
}
// CompareResult is the result of comparison.
//
// See [gotest.tools/v3/assert/cmp.StringResult] for a convenient implementation of
// this interface.
type CompareResult interface {
Success() bool
FailureMessage() string
}
// MatchFileContent is a [PathOp] that updates a [Manifest] to use the provided
// function to determine if a file's content matches the expectation.
func MatchFileContent(f func([]byte) CompareResult) PathOp {
return func(path Path) error {
if m, ok := path.(*filePath); ok {
m.file.compareContentFunc = f
}
return nil
}
}
// MatchFilesWithGlob is a [PathOp] that updates a [Manifest] to match files using
// glob pattern, and check them using the ops.
func MatchFilesWithGlob(glob string, ops ...PathOp) PathOp {
return func(path Path) error {
if m, ok := path.(*directoryPath); ok {
return m.AddGlobFiles(glob, ops...)
}
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
}

276
vendor/gotest.tools/v3/fs/report.go vendored Normal file
View File

@ -0,0 +1,276 @@
package fs
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/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 [gotest.tools/v3/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 removeCarriageReturn(in []byte) []byte {
return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
}
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 := io.ReadAll(x.content)
defer x.content.Close()
yContent, yErr := io.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 x.compareContentFunc != nil {
r := x.compareContentFunc(yContent)
if !r.Success() {
p = append(p, existenceProblem("content", r.FailureMessage()))
}
return p
}
if x.ignoreCariageReturn || y.ignoreCariageReturn {
xContent = removeCarriageReturn(xContent)
yContent = removeCarriageReturn(yContent)
}
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)
xTarget := x.target
yTarget := y.target
if runtime.GOOS == "windows" {
xTarget = strings.ToLower(xTarget)
yTarget = strings.ToLower(yTarget)
}
if xTarget != yTarget {
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
matchedFiles := make(map[string]bool)
for _, name := range sortedKeys(x.items) {
if name == anyFile {
continue
}
matchedFiles[name] = true
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 len(x.filepathGlobs) != 0 {
for _, name := range sortedKeys(y.items) {
m := matchGlob(name, y.items[name], x.filepathGlobs)
matchedFiles[name] = m.match
f = append(f, m.failures...)
}
}
if _, ok := x.items[anyFile]; ok {
return maybeAppendFailure(f, path, p)
}
for _, name := range sortedKeys(y.items) {
if !matchedFiles[name] {
p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type()))
}
}
return maybeAppendFailure(f, path, p)
}
func maybeAppendFailure(failures []failure, path string, problems []problem) []failure {
if len(problems) > 0 {
return append(failures, failure{path: path, problems: problems})
}
return failures
}
func sortedKeys(items map[string]dirEntry) []string {
keys := make([]string, 0, len(items))
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
}
type globMatch struct {
match bool
failures []failure
}
func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch {
m := globMatch{}
for glob, expectedFile := range globs {
ok, err := filepath.Match(glob, name)
if err != nil {
p := errProblem("failed to match glob pattern", err)
f := failure{path: name, problems: []problem{p}}
m.failures = append(m.failures, f)
}
if ok {
m.match = true
m.failures = eqEntry(name, expectedFile.file, yEntry)
return m
}
}
return m
}
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()
}

View File

@ -0,0 +1,161 @@
// Package assert provides internal utilties for assertions.
package assert
import (
"fmt"
"go/ast"
"go/token"
"reflect"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/internal/format"
"gotest.tools/v3/internal/source"
)
// LogT is the subset of testing.T used by the assert package.
type LogT interface {
Log(args ...interface{})
}
type helperT interface {
Helper()
}
const failureMessage = "assertion failed: "
// Eval the comparison and print a failure messages if the comparison has failed.
func Eval(
t LogT,
argSelector argSelector,
comparison interface{},
msgAndArgs ...interface{},
) bool {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
var success bool
switch check := comparison.(type) {
case bool:
if check {
return true
}
logFailureFromBool(t, msgAndArgs...)
// Undocumented legacy comparison without Result type
case func() (success bool, message string):
success = runCompareFunc(t, check, msgAndArgs...)
case nil:
return true
case error:
msg := failureMsgFromError(check)
t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...))
case cmp.Comparison:
success = RunComparison(t, argSelector, check, msgAndArgs...)
case func() cmp.Result:
success = RunComparison(t, argSelector, check, msgAndArgs...)
default:
t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check))
}
return success
}
func runCompareFunc(
t LogT,
f func() (success bool, message string),
msgAndArgs ...interface{},
) bool {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if success, message := f(); !success {
t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...))
return false
}
return true
}
func logFailureFromBool(t LogT, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
const stackIndex = 3 // Assert()/Check(), assert(), logFailureFromBool()
args, err := source.CallExprArgs(stackIndex)
if err != nil {
t.Log(err.Error())
return
}
const comparisonArgIndex = 1 // Assert(t, comparison)
if len(args) <= comparisonArgIndex {
t.Log(failureMessage + "but assert failed to find the expression to print")
return
}
msg, err := boolFailureMessage(args[comparisonArgIndex])
if err != nil {
t.Log(err.Error())
msg = "expression is false"
}
t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...))
}
func failureMsgFromError(err error) string {
// Handle errors with non-nil types
v := reflect.ValueOf(err)
if v.Kind() == reflect.Ptr && v.IsNil() {
return fmt.Sprintf("error is not nil: error has type %T", err)
}
return "error is not nil: " + err.Error()
}
func boolFailureMessage(expr ast.Expr) (string, error) {
if binaryExpr, ok := expr.(*ast.BinaryExpr); ok {
x, err := source.FormatNode(binaryExpr.X)
if err != nil {
return "", err
}
y, err := source.FormatNode(binaryExpr.Y)
if err != nil {
return "", err
}
switch binaryExpr.Op {
case token.NEQ:
return x + " is " + y, nil
case token.EQL:
return x + " is not " + y, nil
case token.GTR:
return x + " is <= " + y, nil
case token.LSS:
return x + " is >= " + y, nil
case token.GEQ:
return x + " is less than " + y, nil
case token.LEQ:
return x + " is greater than " + y, nil
}
}
if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT {
x, err := source.FormatNode(unaryExpr.X)
if err != nil {
return "", err
}
return x + " is true", nil
}
if ident, ok := expr.(*ast.Ident); ok {
return ident.Name + " is false", nil
}
formatted, err := source.FormatNode(expr)
if err != nil {
return "", err
}
return "expression is false: " + formatted, nil
}

View File

@ -0,0 +1,146 @@
package assert
import (
"errors"
"fmt"
"go/ast"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/internal/format"
"gotest.tools/v3/internal/source"
)
// RunComparison and return Comparison.Success. If the comparison fails a messages
// will be printed using t.Log.
func RunComparison(
t LogT,
argSelector argSelector,
f cmp.Comparison,
msgAndArgs ...interface{},
) bool {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
result := f()
if result.Success() {
return true
}
if source.IsUpdate() {
if updater, ok := result.(updateExpected); ok {
const stackIndex = 3 // Assert/Check, assert, RunComparison
err := updater.UpdatedExpected(stackIndex)
switch {
case err == nil:
return true
case errors.Is(err, source.ErrNotFound):
// do nothing, fallthrough to regular failure message
default:
t.Log("failed to update source", err)
return false
}
}
}
var message string
switch typed := result.(type) {
case resultWithComparisonArgs:
const stackIndex = 3 // Assert/Check, assert, RunComparison
args, err := source.CallExprArgs(stackIndex)
if err != nil {
t.Log(err.Error())
}
message = typed.FailureMessage(filterPrintableExpr(argSelector(args)))
case resultBasic:
message = typed.FailureMessage()
default:
message = fmt.Sprintf("comparison returned invalid Result type: %T", result)
}
t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...))
return false
}
type resultWithComparisonArgs interface {
FailureMessage(args []ast.Expr) string
}
type resultBasic interface {
FailureMessage() string
}
type updateExpected interface {
UpdatedExpected(stackIndex int) error
}
// filterPrintableExpr filters the ast.Expr slice to only include Expr that are
// easy to read when printed and contain relevant information to an assertion.
//
// Ident and SelectorExpr are included because they print nicely and the variable
// names may provide additional context to their values.
// BasicLit and CompositeLit are excluded because their source is equivalent to
// their value, which is already available.
// Other types are ignored for now, but could be added if they are relevant.
func filterPrintableExpr(args []ast.Expr) []ast.Expr {
result := make([]ast.Expr, len(args))
for i, arg := range args {
if isShortPrintableExpr(arg) {
result[i] = arg
continue
}
if starExpr, ok := arg.(*ast.StarExpr); ok {
result[i] = starExpr.X
continue
}
}
return result
}
func isShortPrintableExpr(expr ast.Expr) bool {
switch expr.(type) {
case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr:
return true
case *ast.BinaryExpr, *ast.UnaryExpr:
return true
default:
// CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr
return false
}
}
type argSelector func([]ast.Expr) []ast.Expr
// ArgsAfterT selects args starting at position 1. Used when the caller has a
// testing.T as the first argument, and the args to select should follow it.
func ArgsAfterT(args []ast.Expr) []ast.Expr {
if len(args) < 1 {
return nil
}
return args[1:]
}
// ArgsFromComparisonCall selects args from the CallExpression at position 1.
// Used when the caller has a testing.T as the first argument, and the args to
// select are passed to the cmp.Comparison at position 1.
func ArgsFromComparisonCall(args []ast.Expr) []ast.Expr {
if len(args) <= 1 {
return nil
}
if callExpr, ok := args[1].(*ast.CallExpr); ok {
return callExpr.Args
}
return nil
}
// ArgsAtZeroIndex selects args from the CallExpression at position 1.
// Used when the caller accepts a single cmp.Comparison argument.
func ArgsAtZeroIndex(args []ast.Expr) []ast.Expr {
if len(args) == 0 {
return nil
}
if callExpr, ok := args[0].(*ast.CallExpr); ok {
return callExpr.Args
}
return nil
}

View File

@ -0,0 +1,49 @@
/*
Package cleanup handles migration to and support for the Go 1.14+
testing.TB.Cleanup() function.
*/
package cleanup
import (
"os"
"strings"
)
type cleanupT interface {
Cleanup(f func())
}
// implemented by gotest.tools/x/subtest.TestContext
type addCleanupT interface {
AddCleanup(f func())
}
type logT interface {
Log(...interface{})
}
type helperT interface {
Helper()
}
var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true"
// Cleanup registers f as a cleanup function on t if any mechanisms are available.
//
// Skips registering f if TEST_NOCLEANUP is set to true.
func Cleanup(t logT, f func()) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if noCleanup {
t.Log("skipping cleanup because TEST_NOCLEANUP was enabled.")
return
}
if ct, ok := t.(cleanupT); ok {
ct.Cleanup(f)
return
}
if tc, ok := t.(addCleanupT); ok {
tc.AddCleanup(f)
}
}

View File

@ -0,0 +1,27 @@
Copyright (c) 2013, Patrick Mezard
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,427 @@
/*
Package difflib is a partial port of Python difflib module.
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/v3/internal/difflib"
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// Match stores line numbers of size of match
type Match struct {
A int
B int
Size int
}
// OpCode identifies the type of diff
type OpCode struct {
Tag byte
I1 int
I2 int
J1 int
J2 int
}
// SequenceMatcher compares sequence of strings. The basic
// algorithm predates, and is a little fancier than, an algorithm
// published in the late 1980's by Ratcliff and Obershelp under the
// hyperbolic name "gestalt pattern matching". The basic idea is to find
// the longest contiguous matching subsequence that contains no "junk"
// elements (R-O doesn't address junk). The same idea is then applied
// recursively to the pieces of the sequences to the left and to the right
// of the matching subsequence. This does not yield minimal edit
// sequences, but does tend to yield matches that "look right" to people.
//
// SequenceMatcher tries to compute a "human-friendly diff" between two
// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
// longest *contiguous* & junk-free matching subsequence. That's what
// catches peoples' eyes. The Windows(tm) windiff has another interesting
// notion, pairing up elements that appear uniquely in each sequence.
// That, and the method here, appear to yield more intuitive difference
// reports than does diff. This method appears to be the least vulnerable
// to synching up on blocks of "junk lines", though (like blank lines in
// ordinary text files, or maybe "<P>" lines in HTML files). That may be
// because this is the only method of the 3 that has a *concept* of
// "junk" <wink>.
//
// Timing: Basic R-O is cubic time worst case and quadratic time expected
// case. SequenceMatcher is quadratic time for the worst case and has
// expected-case behavior dependent in a complicated way on how many
// elements the sequences have in common; best case time is linear.
type SequenceMatcher struct {
a []string
b []string
b2j map[string][]int
IsJunk func(string) bool
autoJunk bool
bJunk map[string]struct{}
matchingBlocks []Match
fullBCount map[string]int
bPopular map[string]struct{}
opCodes []OpCode
}
// NewMatcher returns a new SequenceMatcher
func NewMatcher(a, b []string) *SequenceMatcher {
m := SequenceMatcher{autoJunk: true}
m.SetSeqs(a, b)
return &m
}
// SetSeqs sets two sequences to be compared.
func (m *SequenceMatcher) SetSeqs(a, b []string) {
m.SetSeq1(a)
m.SetSeq2(b)
}
// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is
// not changed.
//
// SequenceMatcher computes and caches detailed information about the second
// sequence, so if you want to compare one sequence S against many sequences,
// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
// sequences.
//
// See also SetSeqs() and SetSeq2().
func (m *SequenceMatcher) SetSeq1(a []string) {
if &a == &m.a {
return
}
m.a = a
m.matchingBlocks = nil
m.opCodes = nil
}
// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is
// not changed.
func (m *SequenceMatcher) SetSeq2(b []string) {
if &b == &m.b {
return
}
m.b = b
m.matchingBlocks = nil
m.opCodes = nil
m.fullBCount = nil
m.chainB()
}
func (m *SequenceMatcher) chainB() {
// Populate line -> index mapping
b2j := map[string][]int{}
for i, s := range m.b {
indices := b2j[s]
indices = append(indices, i)
b2j[s] = indices
}
// Purge junk elements
m.bJunk = map[string]struct{}{}
if m.IsJunk != nil {
junk := m.bJunk
for s := range b2j {
if m.IsJunk(s) {
junk[s] = struct{}{}
}
}
for s := range junk {
delete(b2j, s)
}
}
// Purge remaining popular elements
popular := map[string]struct{}{}
n := len(m.b)
if m.autoJunk && n >= 200 {
ntest := n/100 + 1
for s, indices := range b2j {
if len(indices) > ntest {
popular[s] = struct{}{}
}
}
for s := range popular {
delete(b2j, s)
}
}
m.bPopular = popular
m.b2j = b2j
}
func (m *SequenceMatcher) isBJunk(s string) bool {
_, ok := m.bJunk[s]
return ok
}
// Find longest matching block in a[alo:ahi] and b[blo:bhi].
//
// If IsJunk is not defined:
//
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
//
// alo <= i <= i+k <= ahi
// blo <= j <= j+k <= bhi
//
// and for all (i',j',k') meeting those conditions,
//
// k >= k'
// i <= i'
// and if i == i', j <= j'
//
// In other words, of all maximal matching blocks, return one that
// starts earliest in a, and of all those maximal matching blocks that
// start earliest in a, return the one that starts earliest in b.
//
// If IsJunk is defined, first the longest matching block is
// determined as above, but with the additional restriction that no
// junk element appears in the block. Then that block is extended as
// far as possible by matching (only) junk elements on both sides. So
// the resulting block never matches on junk except as identical junk
// happens to be adjacent to an "interesting" match.
//
// If no blocks match, return (alo, blo, 0).
func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
// CAUTION: stripping common prefix or suffix would be incorrect.
// E.g.,
// ab
// acab
// Longest matching block is "ab", but if common prefix is
// stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
// strip, so ends up claiming that ab is changed to acab by
// inserting "ca" in the middle. That's minimal but unintuitive:
// "it's obvious" that someone inserted "ac" at the front.
// Windiff ends up at the same place as diff, but by pairing up
// the unique 'b's and then matching the first two 'a's.
besti, bestj, bestsize := alo, blo, 0
// find longest junk-free match
// during an iteration of the loop, j2len[j] = length of longest
// junk-free match ending with a[i-1] and b[j]
j2len := map[int]int{}
for i := alo; i != ahi; i++ {
// look at all instances of a[i] in b; note that because
// b2j has no junk keys, the loop is skipped if a[i] is junk
newj2len := map[int]int{}
for _, j := range m.b2j[m.a[i]] {
// a[i] matches b[j]
if j < blo {
continue
}
if j >= bhi {
break
}
k := j2len[j-1] + 1
newj2len[j] = k
if k > bestsize {
besti, bestj, bestsize = i-k+1, j-k+1, k
}
}
j2len = newj2len
}
// Extend the best by non-junk elements on each end. In particular,
// "popular" non-junk elements aren't in b2j, which greatly speeds
// the inner loop above, but also means "the best" match so far
// doesn't contain any junk *or* popular non-junk elements.
for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
!m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
// Now that we have a wholly interesting match (albeit possibly
// empty!), we may as well suck up the matching junk on each
// side of it too. Can't think of a good reason not to, and it
// saves post-processing the (possibly considerable) expense of
// figuring out what to do with it. In the case of an empty
// interesting match, this is clearly the right thing to do,
// because no other kind of match is possible in the regions.
for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
return Match{A: besti, B: bestj, Size: bestsize}
}
// GetMatchingBlocks returns a list of triples describing matching subsequences.
//
// Each triple is of the form (i, j, n), and means that
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
// adjacent triples in the list, and the second is not the last triple in the
// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
// adjacent equal blocks.
//
// The last triple is a dummy, (len(a), len(b), 0), and is the only
// triple with n==0.
func (m *SequenceMatcher) GetMatchingBlocks() []Match {
if m.matchingBlocks != nil {
return m.matchingBlocks
}
var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
match := m.findLongestMatch(alo, ahi, blo, bhi)
i, j, k := match.A, match.B, match.Size
if match.Size > 0 {
if alo < i && blo < j {
matched = matchBlocks(alo, i, blo, j, matched)
}
matched = append(matched, match)
if i+k < ahi && j+k < bhi {
matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
}
}
return matched
}
matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
// It's possible that we have adjacent equal blocks in the
// matching_blocks list now.
nonAdjacent := []Match{}
i1, j1, k1 := 0, 0, 0
for _, b := range matched {
// Is this block adjacent to i1, j1, k1?
i2, j2, k2 := b.A, b.B, b.Size
if i1+k1 == i2 && j1+k1 == j2 {
// Yes, so collapse them -- this just increases the length of
// the first block by the length of the second, and the first
// block so lengthened remains the block to compare against.
k1 += k2
} else {
// Not adjacent. Remember the first block (k1==0 means it's
// the dummy we started with), and make the second block the
// new block to compare against.
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
i1, j1, k1 = i2, j2, k2
}
}
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
m.matchingBlocks = nonAdjacent
return m.matchingBlocks
}
// GetOpCodes returns a list of 5-tuples describing how to turn a into b.
//
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
// tuple preceding it, and likewise for j1 == the previous j2.
//
// The tags are characters, with these meanings:
//
// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
//
// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
//
// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
//
// 'e' (equal): a[i1:i2] == b[j1:j2]
func (m *SequenceMatcher) GetOpCodes() []OpCode {
if m.opCodes != nil {
return m.opCodes
}
i, j := 0, 0
matching := m.GetMatchingBlocks()
opCodes := make([]OpCode, 0, len(matching))
for _, m := range matching {
// invariant: we've pumped out correct diffs to change
// a[:i] into b[:j], and the next matching block is
// a[ai:ai+size] == b[bj:bj+size]. So we need to pump
// out a diff to change a[i:ai] into b[j:bj], pump out
// the matching block, and move (i,j) beyond the match
ai, bj, size := m.A, m.B, m.Size
tag := byte(0)
if i < ai && j < bj {
tag = 'r'
} else if i < ai {
tag = 'd'
} else if j < bj {
tag = 'i'
}
if tag > 0 {
opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
}
i, j = ai+size, bj+size
// the list of matching blocks is terminated by a
// sentinel with size 0
if size > 0 {
opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
}
}
m.opCodes = opCodes
return m.opCodes
}
// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes.
//
// Return a generator of groups with up to n lines of context.
// Each group is in the same format as returned by GetOpCodes().
func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
if n < 0 {
n = 3
}
codes := m.GetOpCodes()
if len(codes) == 0 {
codes = []OpCode{{'e', 0, 1, 0, 1}}
}
// Fixup leading and trailing groups if they show no changes.
if codes[0].Tag == 'e' {
c := codes[0]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
}
if codes[len(codes)-1].Tag == 'e' {
c := codes[len(codes)-1]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
}
nn := n + n
groups := [][]OpCode{}
group := []OpCode{}
for _, c := range codes {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
// End the current group and start a new one whenever
// there is a large range with no changes.
if c.Tag == 'e' && i2-i1 > nn {
group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
j1, min(j2, j1+n)})
groups = append(groups, group)
group = []OpCode{}
i1, j1 = max(i1, i2-n), max(j1, j2-n)
}
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
}
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
groups = append(groups, group)
}
return groups
}

View File

@ -0,0 +1,162 @@
// Package format provides utilities for formatting diffs and messages.
package format
import (
"bytes"
"fmt"
"strings"
"unicode"
"gotest.tools/v3/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")
}
}

View File

@ -0,0 +1,27 @@
package format // import "gotest.tools/v3/internal/format"
import "fmt"
// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf
func Message(msgAndArgs ...interface{}) string {
switch len(msgAndArgs) {
case 0:
return ""
case 1:
return fmt.Sprintf("%v", msgAndArgs[0])
default:
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
}
}
// WithCustomMessage accepts one or two messages and formats them appropriately
func WithCustomMessage(source string, msgAndArgs ...interface{}) string {
custom := Message(msgAndArgs...)
switch {
case custom == "":
return source
case source == "":
return custom
}
return fmt.Sprintf("%s: %s", source, custom)
}

View File

@ -0,0 +1,52 @@
package source
import (
"fmt"
"go/ast"
"go/token"
)
func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
var matchedNode ast.Node
ast.Inspect(node, func(node ast.Node) bool {
switch {
case node == nil || matchedNode != nil:
return false
case fileset.Position(node.End()).Line == lineNum:
if funcLit, ok := node.(*ast.FuncLit); ok {
matchedNode = funcLit
return false
}
}
return true
})
debug("defer line node: %s", debugFormatNode{matchedNode})
return matchedNode
}
func guessDefer(node ast.Node) (ast.Node, error) {
defers := collectDefers(node)
switch len(defers) {
case 0:
return nil, fmt.Errorf("failed to find expression in defer")
case 1:
return defers[0].Call, nil
default:
return nil, fmt.Errorf(
"ambiguous call expression: multiple (%d) defers in call block",
len(defers))
}
}
func collectDefers(node ast.Node) []*ast.DeferStmt {
var defers []*ast.DeferStmt
ast.Inspect(node, func(node ast.Node) bool {
if d, ok := node.(*ast.DeferStmt); ok {
defers = append(defers, d)
debug("defer: %s", debugFormatNode{d})
return false
}
return true
})
return defers
}

View File

@ -0,0 +1,148 @@
// Package source provides utilities for handling source-code.
package source // import "gotest.tools/v3/internal/source"
import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"runtime"
)
// FormattedCallExprArg returns the argument from an ast.CallExpr at the
// index in the call stack. The argument is formatted using FormatNode.
func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
args, err := CallExprArgs(stackIndex + 1)
if err != nil {
return "", err
}
if argPos >= len(args) {
return "", errors.New("failed to find expression")
}
return FormatNode(args[argPos])
}
// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at
// the index in the call stack.
func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
_, filename, line, ok := runtime.Caller(stackIndex + 1)
if !ok {
return nil, errors.New("failed to get call stack")
}
debug("call stack position: %s:%d", filename, line)
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
if err != nil {
return nil, fmt.Errorf("failed to parse source file %s: %w", filename, err)
}
expr, err := getCallExprArgs(fileset, astFile, line)
if err != nil {
return nil, fmt.Errorf("call from %s:%d: %w", filename, line, err)
}
return expr, nil
}
func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) {
if node := scanToLine(fileset, astFile, lineNum); node != nil {
return node, nil
}
if node := scanToDeferLine(fileset, astFile, lineNum); node != nil {
node, err := guessDefer(node)
if err != nil || node != nil {
return node, err
}
}
return nil, nil
}
func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
var matchedNode ast.Node
ast.Inspect(node, func(node ast.Node) bool {
switch {
case node == nil || matchedNode != nil:
return false
case fileset.Position(node.Pos()).Line == lineNum:
matchedNode = node
return false
}
return true
})
return matchedNode
}
func getCallExprArgs(fileset *token.FileSet, astFile ast.Node, line int) ([]ast.Expr, error) {
node, err := getNodeAtLine(fileset, astFile, line)
switch {
case err != nil:
return nil, err
case node == nil:
return nil, fmt.Errorf("failed to find an expression")
}
debug("found node: %s", debugFormatNode{node})
visitor := &callExprVisitor{}
ast.Walk(visitor, node)
if visitor.expr == nil {
return nil, errors.New("failed to find call expression")
}
debug("callExpr: %s", debugFormatNode{visitor.expr})
return visitor.expr.Args, nil
}
type callExprVisitor struct {
expr *ast.CallExpr
}
func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
if v.expr != nil || node == nil {
return nil
}
debug("visit: %s", debugFormatNode{node})
switch typed := node.(type) {
case *ast.CallExpr:
v.expr = typed
return nil
case *ast.DeferStmt:
ast.Walk(v, typed.Call.Fun)
return nil
}
return v
}
// FormatNode using go/format.Node and return the result as a string
func FormatNode(node ast.Node) (string, error) {
buf := new(bytes.Buffer)
err := format.Node(buf, token.NewFileSet(), node)
return buf.String(), err
}
var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != ""
func debug(format string, args ...interface{}) {
if debugEnabled {
fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...)
}
}
type debugFormatNode struct {
ast.Node
}
func (n debugFormatNode) String() string {
if n.Node == nil {
return "none"
}
out, err := FormatNode(n.Node)
if err != nil {
return fmt.Sprintf("failed to format %s: %s", n.Node, err)
}
return fmt.Sprintf("(%T) %s", n.Node, out)
}

View File

@ -0,0 +1,171 @@
package source
import (
"bytes"
"errors"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"runtime"
"strings"
)
// IsUpdate is returns true if the -update flag is set. It indicates the user
// running the tests would like to update any golden values.
func IsUpdate() bool {
if Update {
return true
}
return flag.Lookup("update").Value.(flag.Getter).Get().(bool)
}
// Update is a shim for testing, and for compatibility with the old -update-golden
// flag.
var Update bool
func init() {
if f := flag.Lookup("update"); f != nil {
getter, ok := f.Value.(flag.Getter)
msg := "some other package defined an incompatible -update flag, expected a flag.Bool"
if !ok {
panic(msg)
}
if _, ok := getter.Get().(bool); !ok {
panic(msg)
}
return
}
flag.Bool("update", false, "update golden values")
}
// ErrNotFound indicates that UpdateExpectedValue failed to find the
// variable to update, likely because it is not a package level variable.
var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value")
// UpdateExpectedValue looks for a package-level variable with a name that
// starts with expected in the arguments to the caller. If the variable is
// found, the value of the variable will be updated to value of the other
// argument to the caller.
func UpdateExpectedValue(stackIndex int, x, y interface{}) error {
_, filename, line, ok := runtime.Caller(stackIndex + 1)
if !ok {
return errors.New("failed to get call stack")
}
debug("call stack position: %s:%d", filename, line)
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse source file %s: %w", filename, err)
}
expr, err := getCallExprArgs(fileset, astFile, line)
if err != nil {
return fmt.Errorf("call from %s:%d: %w", filename, line, err)
}
if len(expr) < 3 {
debug("not enough arguments %d: %v",
len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}
argIndex, ident := getIdentForExpectedValueArg(expr)
if argIndex < 0 || ident == nil {
debug("no arguments started with the word 'expected': %v",
debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}
value := x
if argIndex == 1 {
value = y
}
strValue, ok := value.(string)
if !ok {
debug("value must be type string, got %T", value)
return ErrNotFound
}
return UpdateVariable(filename, fileset, astFile, ident, strValue)
}
// UpdateVariable writes to filename the contents of astFile with the value of
// the variable updated to value.
func UpdateVariable(
filename string,
fileset *token.FileSet,
astFile *ast.File,
ident *ast.Ident,
value string,
) error {
obj := ident.Obj
if obj == nil {
return ErrNotFound
}
if obj.Kind != ast.Con && obj.Kind != ast.Var {
debug("can only update var and const, found %v", obj.Kind)
return ErrNotFound
}
switch decl := obj.Decl.(type) {
case *ast.ValueSpec:
if len(decl.Names) != 1 {
debug("more than one name in ast.ValueSpec")
return ErrNotFound
}
decl.Values[0] = &ast.BasicLit{
Kind: token.STRING,
Value: "`" + value + "`",
}
case *ast.AssignStmt:
if len(decl.Lhs) != 1 {
debug("more than one name in ast.AssignStmt")
return ErrNotFound
}
decl.Rhs[0] = &ast.BasicLit{
Kind: token.STRING,
Value: "`" + value + "`",
}
default:
debug("can only update *ast.ValueSpec, found %T", obj.Decl)
return ErrNotFound
}
var buf bytes.Buffer
if err := format.Node(&buf, fileset, astFile); err != nil {
return fmt.Errorf("failed to format file after update: %w", err)
}
fh, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open file %v: %w", filename, err)
}
if _, err = fh.Write(buf.Bytes()); err != nil {
return fmt.Errorf("failed to write file %v: %w", filename, err)
}
if err := fh.Sync(); err != nil {
return fmt.Errorf("failed to sync file %v: %w", filename, err)
}
return nil
}
func getIdentForExpectedValueArg(expr []ast.Expr) (int, *ast.Ident) {
for i := 1; i < 3; i++ {
switch e := expr[i].(type) {
case *ast.Ident:
if strings.HasPrefix(strings.ToLower(e.Name), "expected") {
return i, e
}
}
}
return -1, nil
}

View File

@ -0,0 +1,35 @@
package source
import (
"runtime"
"strconv"
"strings"
)
// GoVersionLessThan returns true if runtime.Version() is semantically less than
// version major.minor. Returns false if a release version can not be parsed from
// runtime.Version().
func GoVersionLessThan(major, minor int64) bool {
version := runtime.Version()
// not a release version
if !strings.HasPrefix(version, "go") {
return false
}
version = strings.TrimPrefix(version, "go")
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
rMajor, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return false
}
if rMajor != major {
return rMajor < major
}
rMinor, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return false
}
return rMinor < minor
}