Rebase vfs code to use Moby definition of copy driver
This new version of copy will attempt to use reflinks if the underlying file system supports reflink, which should optimize the vfs storage driver for rootless podman and buildah. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
This commit is contained in:
parent
74694d20cd
commit
6c86fe70d3
|
|
@ -0,0 +1,277 @@
|
|||
// +build linux
|
||||
|
||||
package copy
|
||||
|
||||
/*
|
||||
#include <linux/fs.h>
|
||||
|
||||
#ifndef FICLONE
|
||||
#define FICLONE _IOW(0x94, 9, int)
|
||||
#endif
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containers/storage/pkg/pools"
|
||||
"github.com/containers/storage/pkg/system"
|
||||
rsystem "github.com/opencontainers/runc/libcontainer/system"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Mode indicates whether to use hardlink or copy content
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
// Content creates a new file, and copies the content of the file
|
||||
Content Mode = iota
|
||||
// Hardlink creates a new hardlink to the existing file
|
||||
Hardlink
|
||||
)
|
||||
|
||||
func copyRegular(srcPath, dstPath string, fileinfo os.FileInfo, copyWithFileRange, copyWithFileClone *bool) error {
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// If the destination file already exists, we shouldn't blow it away
|
||||
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileinfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if *copyWithFileClone {
|
||||
_, _, err = unix.Syscall(unix.SYS_IOCTL, dstFile.Fd(), C.FICLONE, srcFile.Fd())
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
*copyWithFileClone = false
|
||||
if err == unix.EXDEV {
|
||||
*copyWithFileRange = false
|
||||
}
|
||||
}
|
||||
if *copyWithFileRange {
|
||||
err = doCopyWithFileRange(srcFile, dstFile, fileinfo)
|
||||
// Trying the file_clone may not have caught the exdev case
|
||||
// as the ioctl may not have been available (therefore EINVAL)
|
||||
if err == unix.EXDEV || err == unix.ENOSYS {
|
||||
*copyWithFileRange = false
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return legacyCopy(srcFile, dstFile)
|
||||
}
|
||||
|
||||
func doCopyWithFileRange(srcFile, dstFile *os.File, fileinfo os.FileInfo) error {
|
||||
amountLeftToCopy := fileinfo.Size()
|
||||
|
||||
for amountLeftToCopy > 0 {
|
||||
n, err := unix.CopyFileRange(int(srcFile.Fd()), nil, int(dstFile.Fd()), nil, int(amountLeftToCopy), 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amountLeftToCopy = amountLeftToCopy - int64(n)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func legacyCopy(srcFile io.Reader, dstFile io.Writer) error {
|
||||
_, err := pools.Copy(dstFile, srcFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func copyXattr(srcPath, dstPath, attr string) error {
|
||||
data, err := system.Lgetxattr(srcPath, attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if data != nil {
|
||||
if err := system.Lsetxattr(dstPath, attr, data, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileID struct {
|
||||
dev uint64
|
||||
ino uint64
|
||||
}
|
||||
|
||||
type dirMtimeInfo struct {
|
||||
dstPath *string
|
||||
stat *syscall.Stat_t
|
||||
}
|
||||
|
||||
// DirCopy copies or hardlinks the contents of one directory to another,
|
||||
// properly handling xattrs, and soft links
|
||||
//
|
||||
// Copying xattrs can be opted out of by passing false for copyXattrs.
|
||||
func DirCopy(srcDir, dstDir string, copyMode Mode, copyXattrs bool) error {
|
||||
copyWithFileRange := true
|
||||
copyWithFileClone := true
|
||||
|
||||
// This is a map of source file inodes to dst file paths
|
||||
copiedFiles := make(map[fileID]string)
|
||||
|
||||
dirsToSetMtimes := list.New()
|
||||
err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rebase path
|
||||
relPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dstDir, relPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat, ok := f.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return fmt.Errorf("Unable to get raw syscall.Stat_t data for %s", srcPath)
|
||||
}
|
||||
|
||||
isHardlink := false
|
||||
|
||||
switch f.Mode() & os.ModeType {
|
||||
case 0: // Regular file
|
||||
id := fileID{dev: stat.Dev, ino: stat.Ino}
|
||||
if copyMode == Hardlink {
|
||||
isHardlink = true
|
||||
if err2 := os.Link(srcPath, dstPath); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
} else if hardLinkDstPath, ok := copiedFiles[id]; ok {
|
||||
if err2 := os.Link(hardLinkDstPath, dstPath); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
} else {
|
||||
if err2 := copyRegular(srcPath, dstPath, f, ©WithFileRange, ©WithFileClone); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
copiedFiles[id] = dstPath
|
||||
}
|
||||
|
||||
case os.ModeDir:
|
||||
if err := os.Mkdir(dstPath, f.Mode()); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
case os.ModeSymlink:
|
||||
link, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Symlink(link, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case os.ModeNamedPipe:
|
||||
fallthrough
|
||||
case os.ModeSocket:
|
||||
if err := unix.Mkfifo(dstPath, stat.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case os.ModeDevice:
|
||||
if rsystem.RunningInUserNS() {
|
||||
// cannot create a device if running in user namespace
|
||||
return nil
|
||||
}
|
||||
if err := unix.Mknod(dstPath, stat.Mode, int(stat.Rdev)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown file type for %s", srcPath)
|
||||
}
|
||||
|
||||
// Everything below is copying metadata from src to dst. All this metadata
|
||||
// already shares an inode for hardlinks.
|
||||
if isHardlink {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Lchown(dstPath, int(stat.Uid), int(stat.Gid)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if copyXattrs {
|
||||
if err := doCopyXattrs(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
isSymlink := f.Mode()&os.ModeSymlink != 0
|
||||
|
||||
// There is no LChmod, so ignore mode for symlink. Also, this
|
||||
// must happen after chown, as that can modify the file mode
|
||||
if !isSymlink {
|
||||
if err := os.Chmod(dstPath, f.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// system.Chtimes doesn't support a NOFOLLOW flag atm
|
||||
// nolint: unconvert
|
||||
if f.IsDir() {
|
||||
dirsToSetMtimes.PushFront(&dirMtimeInfo{dstPath: &dstPath, stat: stat})
|
||||
} else if !isSymlink {
|
||||
aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
|
||||
mTime := time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec))
|
||||
if err := system.Chtimes(dstPath, aTime, mTime); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ts := []syscall.Timespec{stat.Atim, stat.Mtim}
|
||||
if err := system.LUtimesNano(dstPath, ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for e := dirsToSetMtimes.Front(); e != nil; e = e.Next() {
|
||||
mtimeInfo := e.Value.(*dirMtimeInfo)
|
||||
ts := []syscall.Timespec{mtimeInfo.stat.Atim, mtimeInfo.stat.Mtim}
|
||||
if err := system.LUtimesNano(*mtimeInfo.dstPath, ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doCopyXattrs(srcPath, dstPath string) error {
|
||||
if err := copyXattr(srcPath, dstPath, "security.capability"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to copy this attribute if it appears in an overlay upper layer, as
|
||||
// this function is used to copy those. It is set by overlay if a directory
|
||||
// is removed and then re-created and should not inherit anything from the
|
||||
// same dir in the lower dir.
|
||||
return copyXattr(srcPath, dstPath, "trusted.overlay.opaque")
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// +build linux
|
||||
|
||||
package copy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containers/storage/pkg/system"
|
||||
"golang.org/x/sys/unix"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
copyWithFileRange := true
|
||||
copyWithFileClone := true
|
||||
doCopyTest(t, ©WithFileRange, ©WithFileClone)
|
||||
}
|
||||
|
||||
func TestCopyWithoutRange(t *testing.T) {
|
||||
copyWithFileRange := false
|
||||
copyWithFileClone := false
|
||||
doCopyTest(t, ©WithFileRange, ©WithFileClone)
|
||||
}
|
||||
|
||||
func TestCopyDir(t *testing.T) {
|
||||
srcDir, err := ioutil.TempDir("", "srcDir")
|
||||
assert.NilError(t, err)
|
||||
populateSrcDir(t, srcDir, 3)
|
||||
|
||||
dstDir, err := ioutil.TempDir("", "testdst")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(dstDir)
|
||||
|
||||
assert.Check(t, DirCopy(srcDir, dstDir, Content, false))
|
||||
assert.NilError(t, filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rebase path
|
||||
relPath, err := filepath.Rel(srcDir, srcPath)
|
||||
assert.NilError(t, err)
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dstDir, relPath)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// If we add non-regular dirs and files to the test
|
||||
// then we need to add more checks here.
|
||||
dstFileInfo, err := os.Lstat(dstPath)
|
||||
assert.NilError(t, err)
|
||||
|
||||
srcFileSys := f.Sys().(*syscall.Stat_t)
|
||||
dstFileSys := dstFileInfo.Sys().(*syscall.Stat_t)
|
||||
|
||||
t.Log(relPath)
|
||||
if srcFileSys.Dev == dstFileSys.Dev {
|
||||
assert.Check(t, srcFileSys.Ino != dstFileSys.Ino)
|
||||
}
|
||||
// Todo: check size, and ctim is not equal
|
||||
/// on filesystems that have granular ctimes
|
||||
assert.Check(t, is.DeepEqual(srcFileSys.Mode, dstFileSys.Mode))
|
||||
assert.Check(t, is.DeepEqual(srcFileSys.Uid, dstFileSys.Uid))
|
||||
assert.Check(t, is.DeepEqual(srcFileSys.Gid, dstFileSys.Gid))
|
||||
assert.Check(t, is.DeepEqual(srcFileSys.Mtim, dstFileSys.Mtim))
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func randomMode(baseMode int) os.FileMode {
|
||||
for i := 0; i < 7; i++ {
|
||||
baseMode = baseMode | (1&rand.Intn(2))<<uint(i)
|
||||
}
|
||||
return os.FileMode(baseMode)
|
||||
}
|
||||
|
||||
func populateSrcDir(t *testing.T, srcDir string, remainingDepth int) {
|
||||
if remainingDepth == 0 {
|
||||
return
|
||||
}
|
||||
aTime := time.Unix(rand.Int63(), 0)
|
||||
mTime := time.Unix(rand.Int63(), 0)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
dirName := filepath.Join(srcDir, fmt.Sprintf("srcdir-%d", i))
|
||||
// Owner all bits set
|
||||
assert.NilError(t, os.Mkdir(dirName, randomMode(0700)))
|
||||
populateSrcDir(t, dirName, remainingDepth-1)
|
||||
assert.NilError(t, system.Chtimes(dirName, aTime, mTime))
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
fileName := filepath.Join(srcDir, fmt.Sprintf("srcfile-%d", i))
|
||||
// Owner read bit set
|
||||
assert.NilError(t, ioutil.WriteFile(fileName, []byte{}, randomMode(0400)))
|
||||
assert.NilError(t, system.Chtimes(fileName, aTime, mTime))
|
||||
}
|
||||
}
|
||||
|
||||
func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
|
||||
dir, err := ioutil.TempDir("", "storage-copy-check")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
srcFilename := filepath.Join(dir, "srcFilename")
|
||||
dstFilename := filepath.Join(dir, "dstilename")
|
||||
|
||||
r := rand.New(rand.NewSource(0))
|
||||
buf := make([]byte, 1024)
|
||||
_, err = r.Read(buf)
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, ioutil.WriteFile(srcFilename, buf, 0777))
|
||||
fileinfo, err := os.Stat(srcFilename)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.NilError(t, copyRegular(srcFilename, dstFilename, fileinfo, copyWithFileRange, copyWithFileClone))
|
||||
readBuf, err := ioutil.ReadFile(dstFilename)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(buf, readBuf))
|
||||
}
|
||||
|
||||
func TestCopyHardlink(t *testing.T) {
|
||||
var srcFile1FileInfo, srcFile2FileInfo, dstFile1FileInfo, dstFile2FileInfo unix.Stat_t
|
||||
|
||||
srcDir, err := ioutil.TempDir("", "srcDir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(srcDir)
|
||||
|
||||
dstDir, err := ioutil.TempDir("", "dstDir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(dstDir)
|
||||
|
||||
srcFile1 := filepath.Join(srcDir, "file1")
|
||||
srcFile2 := filepath.Join(srcDir, "file2")
|
||||
dstFile1 := filepath.Join(dstDir, "file1")
|
||||
dstFile2 := filepath.Join(dstDir, "file2")
|
||||
assert.NilError(t, ioutil.WriteFile(srcFile1, []byte{}, 0777))
|
||||
assert.NilError(t, os.Link(srcFile1, srcFile2))
|
||||
|
||||
assert.Check(t, DirCopy(srcDir, dstDir, Content, false))
|
||||
|
||||
assert.NilError(t, unix.Stat(srcFile1, &srcFile1FileInfo))
|
||||
assert.NilError(t, unix.Stat(srcFile2, &srcFile2FileInfo))
|
||||
assert.Equal(t, srcFile1FileInfo.Ino, srcFile2FileInfo.Ino)
|
||||
|
||||
assert.NilError(t, unix.Stat(dstFile1, &dstFile1FileInfo))
|
||||
assert.NilError(t, unix.Stat(dstFile2, &dstFile2FileInfo))
|
||||
assert.Check(t, is.Equal(dstFile1FileInfo.Ino, dstFile2FileInfo.Ino))
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package vfs
|
||||
|
||||
import "github.com/containers/storage/drivers/copy"
|
||||
|
||||
func dirCopy(srcDir, dstDir string) error {
|
||||
return copy.DirCopy(srcDir, dstDir, copy.Content, false)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// +build !linux
|
||||
|
||||
package vfs // import "github.com/containers/storage/drivers/vfs"
|
||||
|
||||
import "github.com/containers/storage/pkg/chrootarchive"
|
||||
|
||||
func dirCopy(srcDir, dstDir string) error {
|
||||
return chrootarchive.NewArchiver(nil).CopyWithTar(srcDir, dstDir)
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/containers/storage/drivers"
|
||||
"github.com/containers/storage/pkg/chrootarchive"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/ostree"
|
||||
"github.com/containers/storage/pkg/system"
|
||||
|
|
@ -15,8 +14,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// CopyWithTar defines the copy method to use.
|
||||
CopyWithTar = chrootarchive.NewArchiver(nil).CopyWithTar
|
||||
// CopyDir defines the copy method to use.
|
||||
CopyDir = dirCopy
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -141,7 +140,7 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, ro bool
|
|||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", parent, err)
|
||||
}
|
||||
if err := CopyWithTar(parentDir, dir); err != nil {
|
||||
if err := dirCopy(parentDir, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ github.com/tchap/go-patricia v2.2.6
|
|||
github.com/vbatts/tar-split v0.10.2
|
||||
golang.org/x/net 7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6
|
||||
golang.org/x/sys 07c182904dbd53199946ba614a412c61d3c548f5
|
||||
gotest.tools master
|
||||
github.com/google/go-cmp master
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2017 The Go Authors. 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.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER 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.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Package for equality of Go values
|
||||
|
||||
[][godoc]
|
||||
[][travis]
|
||||
|
||||
This package is intended to be a more powerful and safer alternative to
|
||||
`reflect.DeepEqual` for comparing whether two values are semantically equal.
|
||||
|
||||
The primary features of `cmp` are:
|
||||
|
||||
* When the default behavior of equality does not suit the needs of the test,
|
||||
custom equality functions can override the equality operation.
|
||||
For example, an equality function may report floats as equal so long as they
|
||||
are within some tolerance of each other.
|
||||
|
||||
* Types that have an `Equal` method may use that method to determine equality.
|
||||
This allows package authors to determine the equality operation for the types
|
||||
that they define.
|
||||
|
||||
* If no custom equality functions are used and no `Equal` method is defined,
|
||||
equality is determined by recursively comparing the primitive kinds on both
|
||||
values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported
|
||||
fields are not compared by default; they result in panics unless suppressed
|
||||
by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly
|
||||
compared using the `AllowUnexported` option.
|
||||
|
||||
See the [GoDoc documentation][godoc] for more information.
|
||||
|
||||
This is not an official Google product.
|
||||
|
||||
[godoc]: https://godoc.org/github.com/google/go-cmp/cmp
|
||||
[travis]: https://travis-ci.org/google/go-cmp
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go get -u github.com/google/go-cmp/cmp
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD - See [LICENSE][license] file
|
||||
|
||||
[license]: https://github.com/google/go-cmp/blob/master/LICENSE
|
||||
|
|
@ -0,0 +1,599 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// Package cmp determines equality of values.
|
||||
//
|
||||
// This package is intended to be a more powerful and safer alternative to
|
||||
// reflect.DeepEqual for comparing whether two values are semantically equal.
|
||||
//
|
||||
// The primary features of cmp are:
|
||||
//
|
||||
// • When the default behavior of equality does not suit the needs of the test,
|
||||
// custom equality functions can override the equality operation.
|
||||
// For example, an equality function may report floats as equal so long as they
|
||||
// are within some tolerance of each other.
|
||||
//
|
||||
// • Types that have an Equal method may use that method to determine equality.
|
||||
// This allows package authors to determine the equality operation for the types
|
||||
// that they define.
|
||||
//
|
||||
// • If no custom equality functions are used and no Equal method is defined,
|
||||
// equality is determined by recursively comparing the primitive kinds on both
|
||||
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
|
||||
// fields are not compared by default; they result in panics unless suppressed
|
||||
// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared
|
||||
// using the AllowUnexported option.
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp/internal/diff"
|
||||
"github.com/google/go-cmp/cmp/internal/function"
|
||||
"github.com/google/go-cmp/cmp/internal/value"
|
||||
)
|
||||
|
||||
// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to
|
||||
// the reflection package's inability to retrieve such entries. Equal will panic
|
||||
// anytime it comes across a NaN key, but this behavior may change.
|
||||
//
|
||||
// See https://golang.org/issue/11104 for more details.
|
||||
|
||||
var nothing = reflect.Value{}
|
||||
|
||||
// Equal reports whether x and y are equal by recursively applying the
|
||||
// following rules in the given order to x and y and all of their sub-values:
|
||||
//
|
||||
// • If two values are not of the same type, then they are never equal
|
||||
// and the overall result is false.
|
||||
//
|
||||
// • Let S be the set of all Ignore, Transformer, and Comparer options that
|
||||
// remain after applying all path filters, value filters, and type filters.
|
||||
// If at least one Ignore exists in S, then the comparison is ignored.
|
||||
// If the number of Transformer and Comparer options in S is greater than one,
|
||||
// then Equal panics because it is ambiguous which option to use.
|
||||
// If S contains a single Transformer, then use that to transform the current
|
||||
// values and recursively call Equal on the output values.
|
||||
// If S contains a single Comparer, then use that to compare the current values.
|
||||
// Otherwise, evaluation proceeds to the next rule.
|
||||
//
|
||||
// • If the values have an Equal method of the form "(T) Equal(T) bool" or
|
||||
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
|
||||
// x.Equal(y) even if x or y is nil.
|
||||
// Otherwise, no such method exists and evaluation proceeds to the next rule.
|
||||
//
|
||||
// • Lastly, try to compare x and y based on their basic kinds.
|
||||
// Simple kinds like booleans, integers, floats, complex numbers, strings, and
|
||||
// channels are compared using the equivalent of the == operator in Go.
|
||||
// Functions are only equal if they are both nil, otherwise they are unequal.
|
||||
// Pointers are equal if the underlying values they point to are also equal.
|
||||
// Interfaces are equal if their underlying concrete values are also equal.
|
||||
//
|
||||
// Structs are equal if all of their fields are equal. If a struct contains
|
||||
// unexported fields, Equal panics unless the AllowUnexported option is used or
|
||||
// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field.
|
||||
//
|
||||
// Arrays, slices, and maps are equal if they are both nil or both non-nil
|
||||
// with the same length and the elements at each index or key are equal.
|
||||
// Note that a non-nil empty slice and a nil slice are not equal.
|
||||
// To equate empty slices and maps, consider using cmpopts.EquateEmpty.
|
||||
// Map keys are equal according to the == operator.
|
||||
// To use custom comparisons for map keys, consider using cmpopts.SortMaps.
|
||||
func Equal(x, y interface{}, opts ...Option) bool {
|
||||
s := newState(opts)
|
||||
s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y))
|
||||
return s.result.Equal()
|
||||
}
|
||||
|
||||
// Diff returns a human-readable report of the differences between two values.
|
||||
// It returns an empty string if and only if Equal returns true for the same
|
||||
// input values and options. The output string will use the "-" symbol to
|
||||
// indicate elements removed from x, and the "+" symbol to indicate elements
|
||||
// added to y.
|
||||
//
|
||||
// Do not depend on this output being stable.
|
||||
func Diff(x, y interface{}, opts ...Option) string {
|
||||
r := new(defaultReporter)
|
||||
opts = Options{Options(opts), r}
|
||||
eq := Equal(x, y, opts...)
|
||||
d := r.String()
|
||||
if (d == "") != eq {
|
||||
panic("inconsistent difference and equality results")
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
type state struct {
|
||||
// These fields represent the "comparison state".
|
||||
// Calling statelessCompare must not result in observable changes to these.
|
||||
result diff.Result // The current result of comparison
|
||||
curPath Path // The current path in the value tree
|
||||
reporter reporter // Optional reporter used for difference formatting
|
||||
|
||||
// recChecker checks for infinite cycles applying the same set of
|
||||
// transformers upon the output of itself.
|
||||
recChecker recChecker
|
||||
|
||||
// dynChecker triggers pseudo-random checks for option correctness.
|
||||
// It is safe for statelessCompare to mutate this value.
|
||||
dynChecker dynChecker
|
||||
|
||||
// These fields, once set by processOption, will not change.
|
||||
exporters map[reflect.Type]bool // Set of structs with unexported field visibility
|
||||
opts Options // List of all fundamental and filter options
|
||||
}
|
||||
|
||||
func newState(opts []Option) *state {
|
||||
s := new(state)
|
||||
for _, opt := range opts {
|
||||
s.processOption(opt)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *state) processOption(opt Option) {
|
||||
switch opt := opt.(type) {
|
||||
case nil:
|
||||
case Options:
|
||||
for _, o := range opt {
|
||||
s.processOption(o)
|
||||
}
|
||||
case coreOption:
|
||||
type filtered interface {
|
||||
isFiltered() bool
|
||||
}
|
||||
if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
|
||||
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
|
||||
}
|
||||
s.opts = append(s.opts, opt)
|
||||
case visibleStructs:
|
||||
if s.exporters == nil {
|
||||
s.exporters = make(map[reflect.Type]bool)
|
||||
}
|
||||
for t := range opt {
|
||||
s.exporters[t] = true
|
||||
}
|
||||
case reporter:
|
||||
if s.reporter != nil {
|
||||
panic("difference reporter already registered")
|
||||
}
|
||||
s.reporter = opt
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown option %T", opt))
|
||||
}
|
||||
}
|
||||
|
||||
// statelessCompare compares two values and returns the result.
|
||||
// This function is stateless in that it does not alter the current result,
|
||||
// or output to any registered reporters.
|
||||
func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result {
|
||||
// We do not save and restore the curPath because all of the compareX
|
||||
// methods should properly push and pop from the path.
|
||||
// It is an implementation bug if the contents of curPath differs from
|
||||
// when calling this function to when returning from it.
|
||||
|
||||
oldResult, oldReporter := s.result, s.reporter
|
||||
s.result = diff.Result{} // Reset result
|
||||
s.reporter = nil // Remove reporter to avoid spurious printouts
|
||||
s.compareAny(vx, vy)
|
||||
res := s.result
|
||||
s.result, s.reporter = oldResult, oldReporter
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *state) compareAny(vx, vy reflect.Value) {
|
||||
// TODO: Support cyclic data structures.
|
||||
s.recChecker.Check(s.curPath)
|
||||
|
||||
// Rule 0: Differing types are never equal.
|
||||
if !vx.IsValid() || !vy.IsValid() {
|
||||
s.report(vx.IsValid() == vy.IsValid(), vx, vy)
|
||||
return
|
||||
}
|
||||
if vx.Type() != vy.Type() {
|
||||
s.report(false, vx, vy) // Possible for path to be empty
|
||||
return
|
||||
}
|
||||
t := vx.Type()
|
||||
if len(s.curPath) == 0 {
|
||||
s.curPath.push(&pathStep{typ: t})
|
||||
defer s.curPath.pop()
|
||||
}
|
||||
vx, vy = s.tryExporting(vx, vy)
|
||||
|
||||
// Rule 1: Check whether an option applies on this node in the value tree.
|
||||
if s.tryOptions(vx, vy, t) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rule 2: Check whether the type has a valid Equal method.
|
||||
if s.tryMethod(vx, vy, t) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rule 3: Recursively descend into each value's underlying kind.
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
s.report(vx.Bool() == vy.Bool(), vx, vy)
|
||||
return
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
s.report(vx.Int() == vy.Int(), vx, vy)
|
||||
return
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
s.report(vx.Uint() == vy.Uint(), vx, vy)
|
||||
return
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s.report(vx.Float() == vy.Float(), vx, vy)
|
||||
return
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
s.report(vx.Complex() == vy.Complex(), vx, vy)
|
||||
return
|
||||
case reflect.String:
|
||||
s.report(vx.String() == vy.String(), vx, vy)
|
||||
return
|
||||
case reflect.Chan, reflect.UnsafePointer:
|
||||
s.report(vx.Pointer() == vy.Pointer(), vx, vy)
|
||||
return
|
||||
case reflect.Func:
|
||||
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
|
||||
return
|
||||
case reflect.Ptr:
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
|
||||
return
|
||||
}
|
||||
s.curPath.push(&indirect{pathStep{t.Elem()}})
|
||||
defer s.curPath.pop()
|
||||
s.compareAny(vx.Elem(), vy.Elem())
|
||||
return
|
||||
case reflect.Interface:
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
|
||||
return
|
||||
}
|
||||
if vx.Elem().Type() != vy.Elem().Type() {
|
||||
s.report(false, vx.Elem(), vy.Elem())
|
||||
return
|
||||
}
|
||||
s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}})
|
||||
defer s.curPath.pop()
|
||||
s.compareAny(vx.Elem(), vy.Elem())
|
||||
return
|
||||
case reflect.Slice:
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
s.compareArray(vx, vy, t)
|
||||
return
|
||||
case reflect.Map:
|
||||
s.compareMap(vx, vy, t)
|
||||
return
|
||||
case reflect.Struct:
|
||||
s.compareStruct(vx, vy, t)
|
||||
return
|
||||
default:
|
||||
panic(fmt.Sprintf("%v kind not handled", t.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) {
|
||||
if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported {
|
||||
if sf.force {
|
||||
// Use unsafe pointer arithmetic to get read-write access to an
|
||||
// unexported field in the struct.
|
||||
vx = unsafeRetrieveField(sf.pvx, sf.field)
|
||||
vy = unsafeRetrieveField(sf.pvy, sf.field)
|
||||
} else {
|
||||
// We are not allowed to export the value, so invalidate them
|
||||
// so that tryOptions can panic later if not explicitly ignored.
|
||||
vx = nothing
|
||||
vy = nothing
|
||||
}
|
||||
}
|
||||
return vx, vy
|
||||
}
|
||||
|
||||
func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
|
||||
// If there were no FilterValues, we will not detect invalid inputs,
|
||||
// so manually check for them and append invalid if necessary.
|
||||
// We still evaluate the options since an ignore can override invalid.
|
||||
opts := s.opts
|
||||
if !vx.IsValid() || !vy.IsValid() {
|
||||
opts = Options{opts, invalid{}}
|
||||
}
|
||||
|
||||
// Evaluate all filters and apply the remaining options.
|
||||
if opt := opts.filter(s, vx, vy, t); opt != nil {
|
||||
opt.apply(s, vx, vy)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {
|
||||
// Check if this type even has an Equal method.
|
||||
m, ok := t.MethodByName("Equal")
|
||||
if !ok || !function.IsType(m.Type, function.EqualAssignable) {
|
||||
return false
|
||||
}
|
||||
|
||||
eq := s.callTTBFunc(m.Func, vx, vy)
|
||||
s.report(eq, vx, vy)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
|
||||
v = sanitizeValue(v, f.Type().In(0))
|
||||
if !s.dynChecker.Next() {
|
||||
return f.Call([]reflect.Value{v})[0]
|
||||
}
|
||||
|
||||
// Run the function twice and ensure that we get the same results back.
|
||||
// We run in goroutines so that the race detector (if enabled) can detect
|
||||
// unsafe mutations to the input.
|
||||
c := make(chan reflect.Value)
|
||||
go detectRaces(c, f, v)
|
||||
want := f.Call([]reflect.Value{v})[0]
|
||||
if got := <-c; !s.statelessCompare(got, want).Equal() {
|
||||
// To avoid false-positives with non-reflexive equality operations,
|
||||
// we sanity check whether a value is equal to itself.
|
||||
if !s.statelessCompare(want, want).Equal() {
|
||||
return want
|
||||
}
|
||||
fn := getFuncName(f.Pointer())
|
||||
panic(fmt.Sprintf("non-deterministic function detected: %s", fn))
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
|
||||
x = sanitizeValue(x, f.Type().In(0))
|
||||
y = sanitizeValue(y, f.Type().In(1))
|
||||
if !s.dynChecker.Next() {
|
||||
return f.Call([]reflect.Value{x, y})[0].Bool()
|
||||
}
|
||||
|
||||
// Swapping the input arguments is sufficient to check that
|
||||
// f is symmetric and deterministic.
|
||||
// We run in goroutines so that the race detector (if enabled) can detect
|
||||
// unsafe mutations to the input.
|
||||
c := make(chan reflect.Value)
|
||||
go detectRaces(c, f, y, x)
|
||||
want := f.Call([]reflect.Value{x, y})[0].Bool()
|
||||
if got := <-c; !got.IsValid() || got.Bool() != want {
|
||||
fn := getFuncName(f.Pointer())
|
||||
panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn))
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
|
||||
var ret reflect.Value
|
||||
defer func() {
|
||||
recover() // Ignore panics, let the other call to f panic instead
|
||||
c <- ret
|
||||
}()
|
||||
ret = f.Call(vs)[0]
|
||||
}
|
||||
|
||||
// sanitizeValue converts nil interfaces of type T to those of type R,
|
||||
// assuming that T is assignable to R.
|
||||
// Otherwise, it returns the input value as is.
|
||||
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
|
||||
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143).
|
||||
// The upstream fix landed in Go1.10, so we can remove this when drop support
|
||||
// for Go1.9 and below.
|
||||
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
|
||||
return reflect.New(t).Elem()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) {
|
||||
step := &sliceIndex{pathStep{t.Elem()}, 0, 0}
|
||||
s.curPath.push(step)
|
||||
|
||||
// Compute an edit-script for slices vx and vy.
|
||||
es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
|
||||
step.xkey, step.ykey = ix, iy
|
||||
return s.statelessCompare(vx.Index(ix), vy.Index(iy))
|
||||
})
|
||||
|
||||
// Report the entire slice as is if the arrays are of primitive kind,
|
||||
// and the arrays are different enough.
|
||||
isPrimitive := false
|
||||
switch t.Elem().Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
||||
reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
|
||||
isPrimitive = true
|
||||
}
|
||||
if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 {
|
||||
s.curPath.pop() // Pop first since we are reporting the whole slice
|
||||
s.report(false, vx, vy)
|
||||
return
|
||||
}
|
||||
|
||||
// Replay the edit-script.
|
||||
var ix, iy int
|
||||
for _, e := range es {
|
||||
switch e {
|
||||
case diff.UniqueX:
|
||||
step.xkey, step.ykey = ix, -1
|
||||
s.report(false, vx.Index(ix), nothing)
|
||||
ix++
|
||||
case diff.UniqueY:
|
||||
step.xkey, step.ykey = -1, iy
|
||||
s.report(false, nothing, vy.Index(iy))
|
||||
iy++
|
||||
default:
|
||||
step.xkey, step.ykey = ix, iy
|
||||
if e == diff.Identity {
|
||||
s.report(true, vx.Index(ix), vy.Index(iy))
|
||||
} else {
|
||||
s.compareAny(vx.Index(ix), vy.Index(iy))
|
||||
}
|
||||
ix++
|
||||
iy++
|
||||
}
|
||||
}
|
||||
s.curPath.pop()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) {
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
|
||||
return
|
||||
}
|
||||
|
||||
// We combine and sort the two map keys so that we can perform the
|
||||
// comparisons in a deterministic order.
|
||||
step := &mapIndex{pathStep: pathStep{t.Elem()}}
|
||||
s.curPath.push(step)
|
||||
defer s.curPath.pop()
|
||||
for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) {
|
||||
step.key = k
|
||||
vvx := vx.MapIndex(k)
|
||||
vvy := vy.MapIndex(k)
|
||||
switch {
|
||||
case vvx.IsValid() && vvy.IsValid():
|
||||
s.compareAny(vvx, vvy)
|
||||
case vvx.IsValid() && !vvy.IsValid():
|
||||
s.report(false, vvx, nothing)
|
||||
case !vvx.IsValid() && vvy.IsValid():
|
||||
s.report(false, nothing, vvy)
|
||||
default:
|
||||
// It is possible for both vvx and vvy to be invalid if the
|
||||
// key contained a NaN value in it. There is no way in
|
||||
// reflection to be able to retrieve these values.
|
||||
// See https://golang.org/issue/11104
|
||||
panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) {
|
||||
var vax, vay reflect.Value // Addressable versions of vx and vy
|
||||
|
||||
step := &structField{}
|
||||
s.curPath.push(step)
|
||||
defer s.curPath.pop()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
vvx := vx.Field(i)
|
||||
vvy := vy.Field(i)
|
||||
step.typ = t.Field(i).Type
|
||||
step.name = t.Field(i).Name
|
||||
step.idx = i
|
||||
step.unexported = !isExported(step.name)
|
||||
if step.unexported {
|
||||
// Defer checking of unexported fields until later to give an
|
||||
// Ignore a chance to ignore the field.
|
||||
if !vax.IsValid() || !vay.IsValid() {
|
||||
// For unsafeRetrieveField to work, the parent struct must
|
||||
// be addressable. Create a new copy of the values if
|
||||
// necessary to make them addressable.
|
||||
vax = makeAddressable(vx)
|
||||
vay = makeAddressable(vy)
|
||||
}
|
||||
step.force = s.exporters[t]
|
||||
step.pvx = vax
|
||||
step.pvy = vay
|
||||
step.field = t.Field(i)
|
||||
}
|
||||
s.compareAny(vvx, vvy)
|
||||
}
|
||||
}
|
||||
|
||||
// report records the result of a single comparison.
|
||||
// It also calls Report if any reporter is registered.
|
||||
func (s *state) report(eq bool, vx, vy reflect.Value) {
|
||||
if eq {
|
||||
s.result.NSame++
|
||||
} else {
|
||||
s.result.NDiff++
|
||||
}
|
||||
if s.reporter != nil {
|
||||
s.reporter.Report(vx, vy, eq, s.curPath)
|
||||
}
|
||||
}
|
||||
|
||||
// recChecker tracks the state needed to periodically perform checks that
|
||||
// user provided transformers are not stuck in an infinitely recursive cycle.
|
||||
type recChecker struct{ next int }
|
||||
|
||||
// Check scans the Path for any recursive transformers and panics when any
|
||||
// recursive transformers are detected. Note that the presence of a
|
||||
// recursive Transformer does not necessarily imply an infinite cycle.
|
||||
// As such, this check only activates after some minimal number of path steps.
|
||||
func (rc *recChecker) Check(p Path) {
|
||||
const minLen = 1 << 16
|
||||
if rc.next == 0 {
|
||||
rc.next = minLen
|
||||
}
|
||||
if len(p) < rc.next {
|
||||
return
|
||||
}
|
||||
rc.next <<= 1
|
||||
|
||||
// Check whether the same transformer has appeared at least twice.
|
||||
var ss []string
|
||||
m := map[Option]int{}
|
||||
for _, ps := range p {
|
||||
if t, ok := ps.(Transform); ok {
|
||||
t := t.Option()
|
||||
if m[t] == 1 { // Transformer was used exactly once before
|
||||
tf := t.(*transformer).fnc.Type()
|
||||
ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0)))
|
||||
}
|
||||
m[t]++
|
||||
}
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
const warning = "recursive set of Transformers detected"
|
||||
const help = "consider using cmpopts.AcyclicTransformer"
|
||||
set := strings.Join(ss, "\n\t")
|
||||
panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help))
|
||||
}
|
||||
}
|
||||
|
||||
// dynChecker tracks the state needed to periodically perform checks that
|
||||
// user provided functions are symmetric and deterministic.
|
||||
// The zero value is safe for immediate use.
|
||||
type dynChecker struct{ curr, next int }
|
||||
|
||||
// Next increments the state and reports whether a check should be performed.
|
||||
//
|
||||
// Checks occur every Nth function call, where N is a triangular number:
|
||||
// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ...
|
||||
// See https://en.wikipedia.org/wiki/Triangular_number
|
||||
//
|
||||
// This sequence ensures that the cost of checks drops significantly as
|
||||
// the number of functions calls grows larger.
|
||||
func (dc *dynChecker) Next() bool {
|
||||
ok := dc.curr == dc.next
|
||||
if ok {
|
||||
dc.curr = 0
|
||||
dc.next++
|
||||
}
|
||||
dc.curr++
|
||||
return ok
|
||||
}
|
||||
|
||||
// makeAddressable returns a value that is always addressable.
|
||||
// It returns the input verbatim if it is already addressable,
|
||||
// otherwise it creates a new value and returns an addressable copy.
|
||||
func makeAddressable(v reflect.Value) reflect.Value {
|
||||
if v.CanAddr() {
|
||||
return v
|
||||
}
|
||||
vc := reflect.New(v.Type()).Elem()
|
||||
vc.Set(v)
|
||||
return vc
|
||||
}
|
||||
17
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go
generated
vendored
Normal file
17
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build !debug
|
||||
|
||||
package diff
|
||||
|
||||
var debug debugger
|
||||
|
||||
type debugger struct{}
|
||||
|
||||
func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc {
|
||||
return f
|
||||
}
|
||||
func (debugger) Update() {}
|
||||
func (debugger) Finish() {}
|
||||
122
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go
generated
vendored
Normal file
122
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go
generated
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build debug
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The algorithm can be seen running in real-time by enabling debugging:
|
||||
// go test -tags=debug -v
|
||||
//
|
||||
// Example output:
|
||||
// === RUN TestDifference/#34
|
||||
// ┌───────────────────────────────┐
|
||||
// │ \ · · · · · · · · · · · · · · │
|
||||
// │ · # · · · · · · · · · · · · · │
|
||||
// │ · \ · · · · · · · · · · · · · │
|
||||
// │ · · \ · · · · · · · · · · · · │
|
||||
// │ · · · X # · · · · · · · · · · │
|
||||
// │ · · · # \ · · · · · · · · · · │
|
||||
// │ · · · · · # # · · · · · · · · │
|
||||
// │ · · · · · # \ · · · · · · · · │
|
||||
// │ · · · · · · · \ · · · · · · · │
|
||||
// │ · · · · · · · · \ · · · · · · │
|
||||
// │ · · · · · · · · · \ · · · · · │
|
||||
// │ · · · · · · · · · · \ · · # · │
|
||||
// │ · · · · · · · · · · · \ # # · │
|
||||
// │ · · · · · · · · · · · # # # · │
|
||||
// │ · · · · · · · · · · # # # # · │
|
||||
// │ · · · · · · · · · # # # # # · │
|
||||
// │ · · · · · · · · · · · · · · \ │
|
||||
// └───────────────────────────────┘
|
||||
// [.Y..M.XY......YXYXY.|]
|
||||
//
|
||||
// The grid represents the edit-graph where the horizontal axis represents
|
||||
// list X and the vertical axis represents list Y. The start of the two lists
|
||||
// is the top-left, while the ends are the bottom-right. The '·' represents
|
||||
// an unexplored node in the graph. The '\' indicates that the two symbols
|
||||
// from list X and Y are equal. The 'X' indicates that two symbols are similar
|
||||
// (but not exactly equal) to each other. The '#' indicates that the two symbols
|
||||
// are different (and not similar). The algorithm traverses this graph trying to
|
||||
// make the paths starting in the top-left and the bottom-right connect.
|
||||
//
|
||||
// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents
|
||||
// the currently established path from the forward and reverse searches,
|
||||
// separated by a '|' character.
|
||||
|
||||
const (
|
||||
updateDelay = 100 * time.Millisecond
|
||||
finishDelay = 500 * time.Millisecond
|
||||
ansiTerminal = true // ANSI escape codes used to move terminal cursor
|
||||
)
|
||||
|
||||
var debug debugger
|
||||
|
||||
type debugger struct {
|
||||
sync.Mutex
|
||||
p1, p2 EditScript
|
||||
fwdPath, revPath *EditScript
|
||||
grid []byte
|
||||
lines int
|
||||
}
|
||||
|
||||
func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc {
|
||||
dbg.Lock()
|
||||
dbg.fwdPath, dbg.revPath = p1, p2
|
||||
top := "┌─" + strings.Repeat("──", nx) + "┐\n"
|
||||
row := "│ " + strings.Repeat("· ", nx) + "│\n"
|
||||
btm := "└─" + strings.Repeat("──", nx) + "┘\n"
|
||||
dbg.grid = []byte(top + strings.Repeat(row, ny) + btm)
|
||||
dbg.lines = strings.Count(dbg.String(), "\n")
|
||||
fmt.Print(dbg)
|
||||
|
||||
// Wrap the EqualFunc so that we can intercept each result.
|
||||
return func(ix, iy int) (r Result) {
|
||||
cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")]
|
||||
for i := range cell {
|
||||
cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot
|
||||
}
|
||||
switch r = f(ix, iy); {
|
||||
case r.Equal():
|
||||
cell[0] = '\\'
|
||||
case r.Similar():
|
||||
cell[0] = 'X'
|
||||
default:
|
||||
cell[0] = '#'
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (dbg *debugger) Update() {
|
||||
dbg.print(updateDelay)
|
||||
}
|
||||
|
||||
func (dbg *debugger) Finish() {
|
||||
dbg.print(finishDelay)
|
||||
dbg.Unlock()
|
||||
}
|
||||
|
||||
func (dbg *debugger) String() string {
|
||||
dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0]
|
||||
for i := len(*dbg.revPath) - 1; i >= 0; i-- {
|
||||
dbg.p2 = append(dbg.p2, (*dbg.revPath)[i])
|
||||
}
|
||||
return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2)
|
||||
}
|
||||
|
||||
func (dbg *debugger) print(d time.Duration) {
|
||||
if ansiTerminal {
|
||||
fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor
|
||||
}
|
||||
fmt.Print(dbg)
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// Package diff implements an algorithm for producing edit-scripts.
|
||||
// The edit-script is a sequence of operations needed to transform one list
|
||||
// of symbols into another (or vice-versa). The edits allowed are insertions,
|
||||
// deletions, and modifications. The summation of all edits is called the
|
||||
// Levenshtein distance as this problem is well-known in computer science.
|
||||
//
|
||||
// This package prioritizes performance over accuracy. That is, the run time
|
||||
// is more important than obtaining a minimal Levenshtein distance.
|
||||
package diff
|
||||
|
||||
// EditType represents a single operation within an edit-script.
|
||||
type EditType uint8
|
||||
|
||||
const (
|
||||
// Identity indicates that a symbol pair is identical in both list X and Y.
|
||||
Identity EditType = iota
|
||||
// UniqueX indicates that a symbol only exists in X and not Y.
|
||||
UniqueX
|
||||
// UniqueY indicates that a symbol only exists in Y and not X.
|
||||
UniqueY
|
||||
// Modified indicates that a symbol pair is a modification of each other.
|
||||
Modified
|
||||
)
|
||||
|
||||
// EditScript represents the series of differences between two lists.
|
||||
type EditScript []EditType
|
||||
|
||||
// String returns a human-readable string representing the edit-script where
|
||||
// Identity, UniqueX, UniqueY, and Modified are represented by the
|
||||
// '.', 'X', 'Y', and 'M' characters, respectively.
|
||||
func (es EditScript) String() string {
|
||||
b := make([]byte, len(es))
|
||||
for i, e := range es {
|
||||
switch e {
|
||||
case Identity:
|
||||
b[i] = '.'
|
||||
case UniqueX:
|
||||
b[i] = 'X'
|
||||
case UniqueY:
|
||||
b[i] = 'Y'
|
||||
case Modified:
|
||||
b[i] = 'M'
|
||||
default:
|
||||
panic("invalid edit-type")
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// stats returns a histogram of the number of each type of edit operation.
|
||||
func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) {
|
||||
for _, e := range es {
|
||||
switch e {
|
||||
case Identity:
|
||||
s.NI++
|
||||
case UniqueX:
|
||||
s.NX++
|
||||
case UniqueY:
|
||||
s.NY++
|
||||
case Modified:
|
||||
s.NM++
|
||||
default:
|
||||
panic("invalid edit-type")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if
|
||||
// lists X and Y are equal.
|
||||
func (es EditScript) Dist() int { return len(es) - es.stats().NI }
|
||||
|
||||
// LenX is the length of the X list.
|
||||
func (es EditScript) LenX() int { return len(es) - es.stats().NY }
|
||||
|
||||
// LenY is the length of the Y list.
|
||||
func (es EditScript) LenY() int { return len(es) - es.stats().NX }
|
||||
|
||||
// EqualFunc reports whether the symbols at indexes ix and iy are equal.
|
||||
// When called by Difference, the index is guaranteed to be within nx and ny.
|
||||
type EqualFunc func(ix int, iy int) Result
|
||||
|
||||
// Result is the result of comparison.
|
||||
// NSame is the number of sub-elements that are equal.
|
||||
// NDiff is the number of sub-elements that are not equal.
|
||||
type Result struct{ NSame, NDiff int }
|
||||
|
||||
// Equal indicates whether the symbols are equal. Two symbols are equal
|
||||
// if and only if NDiff == 0. If Equal, then they are also Similar.
|
||||
func (r Result) Equal() bool { return r.NDiff == 0 }
|
||||
|
||||
// Similar indicates whether two symbols are similar and may be represented
|
||||
// by using the Modified type. As a special case, we consider binary comparisons
|
||||
// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar.
|
||||
//
|
||||
// The exact ratio of NSame to NDiff to determine similarity may change.
|
||||
func (r Result) Similar() bool {
|
||||
// Use NSame+1 to offset NSame so that binary comparisons are similar.
|
||||
return r.NSame+1 >= r.NDiff
|
||||
}
|
||||
|
||||
// Difference reports whether two lists of lengths nx and ny are equal
|
||||
// given the definition of equality provided as f.
|
||||
//
|
||||
// This function returns an edit-script, which is a sequence of operations
|
||||
// needed to convert one list into the other. The following invariants for
|
||||
// the edit-script are maintained:
|
||||
// • eq == (es.Dist()==0)
|
||||
// • nx == es.LenX()
|
||||
// • ny == es.LenY()
|
||||
//
|
||||
// This algorithm is not guaranteed to be an optimal solution (i.e., one that
|
||||
// produces an edit-script with a minimal Levenshtein distance). This algorithm
|
||||
// favors performance over optimality. The exact output is not guaranteed to
|
||||
// be stable and may change over time.
|
||||
func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
||||
// This algorithm is based on traversing what is known as an "edit-graph".
|
||||
// See Figure 1 from "An O(ND) Difference Algorithm and Its Variations"
|
||||
// by Eugene W. Myers. Since D can be as large as N itself, this is
|
||||
// effectively O(N^2). Unlike the algorithm from that paper, we are not
|
||||
// interested in the optimal path, but at least some "decent" path.
|
||||
//
|
||||
// For example, let X and Y be lists of symbols:
|
||||
// X = [A B C A B B A]
|
||||
// Y = [C B A B A C]
|
||||
//
|
||||
// The edit-graph can be drawn as the following:
|
||||
// A B C A B B A
|
||||
// ┌─────────────┐
|
||||
// C │_|_|\|_|_|_|_│ 0
|
||||
// B │_|\|_|_|\|\|_│ 1
|
||||
// A │\|_|_|\|_|_|\│ 2
|
||||
// B │_|\|_|_|\|\|_│ 3
|
||||
// A │\|_|_|\|_|_|\│ 4
|
||||
// C │ | |\| | | | │ 5
|
||||
// └─────────────┘ 6
|
||||
// 0 1 2 3 4 5 6 7
|
||||
//
|
||||
// List X is written along the horizontal axis, while list Y is written
|
||||
// along the vertical axis. At any point on this grid, if the symbol in
|
||||
// list X matches the corresponding symbol in list Y, then a '\' is drawn.
|
||||
// The goal of any minimal edit-script algorithm is to find a path from the
|
||||
// top-left corner to the bottom-right corner, while traveling through the
|
||||
// fewest horizontal or vertical edges.
|
||||
// A horizontal edge is equivalent to inserting a symbol from list X.
|
||||
// A vertical edge is equivalent to inserting a symbol from list Y.
|
||||
// A diagonal edge is equivalent to a matching symbol between both X and Y.
|
||||
|
||||
// Invariants:
|
||||
// • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx
|
||||
// • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny
|
||||
//
|
||||
// In general:
|
||||
// • fwdFrontier.X < revFrontier.X
|
||||
// • fwdFrontier.Y < revFrontier.Y
|
||||
// Unless, it is time for the algorithm to terminate.
|
||||
fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)}
|
||||
revPath := path{-1, point{nx, ny}, make(EditScript, 0)}
|
||||
fwdFrontier := fwdPath.point // Forward search frontier
|
||||
revFrontier := revPath.point // Reverse search frontier
|
||||
|
||||
// Search budget bounds the cost of searching for better paths.
|
||||
// The longest sequence of non-matching symbols that can be tolerated is
|
||||
// approximately the square-root of the search budget.
|
||||
searchBudget := 4 * (nx + ny) // O(n)
|
||||
|
||||
// The algorithm below is a greedy, meet-in-the-middle algorithm for
|
||||
// computing sub-optimal edit-scripts between two lists.
|
||||
//
|
||||
// The algorithm is approximately as follows:
|
||||
// • Searching for differences switches back-and-forth between
|
||||
// a search that starts at the beginning (the top-left corner), and
|
||||
// a search that starts at the end (the bottom-right corner). The goal of
|
||||
// the search is connect with the search from the opposite corner.
|
||||
// • As we search, we build a path in a greedy manner, where the first
|
||||
// match seen is added to the path (this is sub-optimal, but provides a
|
||||
// decent result in practice). When matches are found, we try the next pair
|
||||
// of symbols in the lists and follow all matches as far as possible.
|
||||
// • When searching for matches, we search along a diagonal going through
|
||||
// through the "frontier" point. If no matches are found, we advance the
|
||||
// frontier towards the opposite corner.
|
||||
// • This algorithm terminates when either the X coordinates or the
|
||||
// Y coordinates of the forward and reverse frontier points ever intersect.
|
||||
//
|
||||
// This algorithm is correct even if searching only in the forward direction
|
||||
// or in the reverse direction. We do both because it is commonly observed
|
||||
// that two lists commonly differ because elements were added to the front
|
||||
// or end of the other list.
|
||||
//
|
||||
// Running the tests with the "debug" build tag prints a visualization of
|
||||
// the algorithm running in real-time. This is educational for understanding
|
||||
// how the algorithm works. See debug_enable.go.
|
||||
f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
|
||||
for {
|
||||
// Forward search from the beginning.
|
||||
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||
break
|
||||
}
|
||||
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||
// Search in a diagonal pattern for a match.
|
||||
z := zigzag(i)
|
||||
p := point{fwdFrontier.X + z, fwdFrontier.Y - z}
|
||||
switch {
|
||||
case p.X >= revPath.X || p.Y < fwdPath.Y:
|
||||
stop1 = true // Hit top-right corner
|
||||
case p.Y >= revPath.Y || p.X < fwdPath.X:
|
||||
stop2 = true // Hit bottom-left corner
|
||||
case f(p.X, p.Y).Equal():
|
||||
// Match found, so connect the path to this point.
|
||||
fwdPath.connect(p, f)
|
||||
fwdPath.append(Identity)
|
||||
// Follow sequence of matches as far as possible.
|
||||
for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
||||
if !f(fwdPath.X, fwdPath.Y).Equal() {
|
||||
break
|
||||
}
|
||||
fwdPath.append(Identity)
|
||||
}
|
||||
fwdFrontier = fwdPath.point
|
||||
stop1, stop2 = true, true
|
||||
default:
|
||||
searchBudget-- // Match not found
|
||||
}
|
||||
debug.Update()
|
||||
}
|
||||
// Advance the frontier towards reverse point.
|
||||
if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y {
|
||||
fwdFrontier.X++
|
||||
} else {
|
||||
fwdFrontier.Y++
|
||||
}
|
||||
|
||||
// Reverse search from the end.
|
||||
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||
break
|
||||
}
|
||||
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||
// Search in a diagonal pattern for a match.
|
||||
z := zigzag(i)
|
||||
p := point{revFrontier.X - z, revFrontier.Y + z}
|
||||
switch {
|
||||
case fwdPath.X >= p.X || revPath.Y < p.Y:
|
||||
stop1 = true // Hit bottom-left corner
|
||||
case fwdPath.Y >= p.Y || revPath.X < p.X:
|
||||
stop2 = true // Hit top-right corner
|
||||
case f(p.X-1, p.Y-1).Equal():
|
||||
// Match found, so connect the path to this point.
|
||||
revPath.connect(p, f)
|
||||
revPath.append(Identity)
|
||||
// Follow sequence of matches as far as possible.
|
||||
for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
||||
if !f(revPath.X-1, revPath.Y-1).Equal() {
|
||||
break
|
||||
}
|
||||
revPath.append(Identity)
|
||||
}
|
||||
revFrontier = revPath.point
|
||||
stop1, stop2 = true, true
|
||||
default:
|
||||
searchBudget-- // Match not found
|
||||
}
|
||||
debug.Update()
|
||||
}
|
||||
// Advance the frontier towards forward point.
|
||||
if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y {
|
||||
revFrontier.X--
|
||||
} else {
|
||||
revFrontier.Y--
|
||||
}
|
||||
}
|
||||
|
||||
// Join the forward and reverse paths and then append the reverse path.
|
||||
fwdPath.connect(revPath.point, f)
|
||||
for i := len(revPath.es) - 1; i >= 0; i-- {
|
||||
t := revPath.es[i]
|
||||
revPath.es = revPath.es[:i]
|
||||
fwdPath.append(t)
|
||||
}
|
||||
debug.Finish()
|
||||
return fwdPath.es
|
||||
}
|
||||
|
||||
type path struct {
|
||||
dir int // +1 if forward, -1 if reverse
|
||||
point // Leading point of the EditScript path
|
||||
es EditScript
|
||||
}
|
||||
|
||||
// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types
|
||||
// to the edit-script to connect p.point to dst.
|
||||
func (p *path) connect(dst point, f EqualFunc) {
|
||||
if p.dir > 0 {
|
||||
// Connect in forward direction.
|
||||
for dst.X > p.X && dst.Y > p.Y {
|
||||
switch r := f(p.X, p.Y); {
|
||||
case r.Equal():
|
||||
p.append(Identity)
|
||||
case r.Similar():
|
||||
p.append(Modified)
|
||||
case dst.X-p.X >= dst.Y-p.Y:
|
||||
p.append(UniqueX)
|
||||
default:
|
||||
p.append(UniqueY)
|
||||
}
|
||||
}
|
||||
for dst.X > p.X {
|
||||
p.append(UniqueX)
|
||||
}
|
||||
for dst.Y > p.Y {
|
||||
p.append(UniqueY)
|
||||
}
|
||||
} else {
|
||||
// Connect in reverse direction.
|
||||
for p.X > dst.X && p.Y > dst.Y {
|
||||
switch r := f(p.X-1, p.Y-1); {
|
||||
case r.Equal():
|
||||
p.append(Identity)
|
||||
case r.Similar():
|
||||
p.append(Modified)
|
||||
case p.Y-dst.Y >= p.X-dst.X:
|
||||
p.append(UniqueY)
|
||||
default:
|
||||
p.append(UniqueX)
|
||||
}
|
||||
}
|
||||
for p.X > dst.X {
|
||||
p.append(UniqueX)
|
||||
}
|
||||
for p.Y > dst.Y {
|
||||
p.append(UniqueY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *path) append(t EditType) {
|
||||
p.es = append(p.es, t)
|
||||
switch t {
|
||||
case Identity, Modified:
|
||||
p.add(p.dir, p.dir)
|
||||
case UniqueX:
|
||||
p.add(p.dir, 0)
|
||||
case UniqueY:
|
||||
p.add(0, p.dir)
|
||||
}
|
||||
debug.Update()
|
||||
}
|
||||
|
||||
type point struct{ X, Y int }
|
||||
|
||||
func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy }
|
||||
|
||||
// zigzag maps a consecutive sequence of integers to a zig-zag sequence.
|
||||
// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...]
|
||||
func zigzag(x int) int {
|
||||
if x&1 != 0 {
|
||||
x = ^x
|
||||
}
|
||||
return x >> 1
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// Package function identifies function types.
|
||||
package function
|
||||
|
||||
import "reflect"
|
||||
|
||||
type funcType int
|
||||
|
||||
const (
|
||||
_ funcType = iota
|
||||
|
||||
ttbFunc // func(T, T) bool
|
||||
tibFunc // func(T, I) bool
|
||||
trFunc // func(T) R
|
||||
|
||||
Equal = ttbFunc // func(T, T) bool
|
||||
EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
|
||||
Transformer = trFunc // func(T) R
|
||||
ValueFilter = ttbFunc // func(T, T) bool
|
||||
Less = ttbFunc // func(T, T) bool
|
||||
)
|
||||
|
||||
var boolType = reflect.TypeOf(true)
|
||||
|
||||
// IsType reports whether the reflect.Type is of the specified function type.
|
||||
func IsType(t reflect.Type, ft funcType) bool {
|
||||
if t == nil || t.Kind() != reflect.Func || t.IsVariadic() {
|
||||
return false
|
||||
}
|
||||
ni, no := t.NumIn(), t.NumOut()
|
||||
switch ft {
|
||||
case ttbFunc: // func(T, T) bool
|
||||
if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
|
||||
return true
|
||||
}
|
||||
case tibFunc: // func(T, I) bool
|
||||
if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
|
||||
return true
|
||||
}
|
||||
case trFunc: // func(T) R
|
||||
if ni == 1 && no == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// Package value provides functionality for reflect.Value types.
|
||||
package value
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
|
||||
// Format formats the value v as a string.
|
||||
//
|
||||
// This is similar to fmt.Sprintf("%+v", v) except this:
|
||||
// * Prints the type unless it can be elided
|
||||
// * Avoids printing struct fields that are zero
|
||||
// * Prints a nil-slice as being nil, not empty
|
||||
// * Prints map entries in deterministic order
|
||||
func Format(v reflect.Value, conf FormatConfig) string {
|
||||
conf.printType = true
|
||||
conf.followPointers = true
|
||||
conf.realPointers = true
|
||||
return formatAny(v, conf, visited{})
|
||||
}
|
||||
|
||||
type FormatConfig struct {
|
||||
UseStringer bool // Should the String method be used if available?
|
||||
printType bool // Should we print the type before the value?
|
||||
PrintPrimitiveType bool // Should we print the type of primitives?
|
||||
followPointers bool // Should we recursively follow pointers?
|
||||
realPointers bool // Should we print the real address of pointers?
|
||||
}
|
||||
|
||||
func formatAny(v reflect.Value, conf FormatConfig, m visited) string {
|
||||
// TODO: Should this be a multi-line printout in certain situations?
|
||||
|
||||
if !v.IsValid() {
|
||||
return "<non-existent>"
|
||||
}
|
||||
if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
|
||||
if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
const stringerPrefix = "s" // Indicates that the String method was used
|
||||
s := v.Interface().(fmt.Stringer).String()
|
||||
return stringerPrefix + formatString(s)
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return formatPrimitive(v.Type(), v.Bool(), conf)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return formatPrimitive(v.Type(), v.Int(), conf)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr {
|
||||
// Unnamed uints are usually bytes or words, so use hexadecimal.
|
||||
return formatPrimitive(v.Type(), formatHex(v.Uint()), conf)
|
||||
}
|
||||
return formatPrimitive(v.Type(), v.Uint(), conf)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return formatPrimitive(v.Type(), v.Float(), conf)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return formatPrimitive(v.Type(), v.Complex(), conf)
|
||||
case reflect.String:
|
||||
return formatPrimitive(v.Type(), formatString(v.String()), conf)
|
||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||
return formatPointer(v, conf)
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
if conf.printType {
|
||||
return fmt.Sprintf("(%v)(nil)", v.Type())
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
if m.Visit(v) || !conf.followPointers {
|
||||
return formatPointer(v, conf)
|
||||
}
|
||||
return "&" + formatAny(v.Elem(), conf, m)
|
||||
case reflect.Interface:
|
||||
if v.IsNil() {
|
||||
if conf.printType {
|
||||
return fmt.Sprintf("%v(nil)", v.Type())
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
return formatAny(v.Elem(), conf, m)
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
if conf.printType {
|
||||
return fmt.Sprintf("%v(nil)", v.Type())
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
var ss []string
|
||||
subConf := conf
|
||||
subConf.printType = v.Type().Elem().Kind() == reflect.Interface
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
vi := v.Index(i)
|
||||
if vi.CanAddr() { // Check for recursive elements
|
||||
p := vi.Addr()
|
||||
if m.Visit(p) {
|
||||
subConf := conf
|
||||
subConf.printType = true
|
||||
ss = append(ss, "*"+formatPointer(p, subConf))
|
||||
continue
|
||||
}
|
||||
}
|
||||
ss = append(ss, formatAny(vi, subConf, m))
|
||||
}
|
||||
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||
if conf.printType {
|
||||
return v.Type().String() + s
|
||||
}
|
||||
return s
|
||||
case reflect.Map:
|
||||
if v.IsNil() {
|
||||
if conf.printType {
|
||||
return fmt.Sprintf("%v(nil)", v.Type())
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
if m.Visit(v) {
|
||||
return formatPointer(v, conf)
|
||||
}
|
||||
|
||||
var ss []string
|
||||
keyConf, valConf := conf, conf
|
||||
keyConf.printType = v.Type().Key().Kind() == reflect.Interface
|
||||
keyConf.followPointers = false
|
||||
valConf.printType = v.Type().Elem().Kind() == reflect.Interface
|
||||
for _, k := range SortKeys(v.MapKeys()) {
|
||||
sk := formatAny(k, keyConf, m)
|
||||
sv := formatAny(v.MapIndex(k), valConf, m)
|
||||
ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
|
||||
}
|
||||
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||
if conf.printType {
|
||||
return v.Type().String() + s
|
||||
}
|
||||
return s
|
||||
case reflect.Struct:
|
||||
var ss []string
|
||||
subConf := conf
|
||||
subConf.printType = true
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
vv := v.Field(i)
|
||||
if isZero(vv) {
|
||||
continue // Elide zero value fields
|
||||
}
|
||||
name := v.Type().Field(i).Name
|
||||
subConf.UseStringer = conf.UseStringer
|
||||
s := formatAny(vv, subConf, m)
|
||||
ss = append(ss, fmt.Sprintf("%s: %s", name, s))
|
||||
}
|
||||
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||
if conf.printType {
|
||||
return v.Type().String() + s
|
||||
}
|
||||
return s
|
||||
default:
|
||||
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func formatString(s string) string {
|
||||
// Use quoted string if it the same length as a raw string literal.
|
||||
// Otherwise, attempt to use the raw string form.
|
||||
qs := strconv.Quote(s)
|
||||
if len(qs) == 1+len(s)+1 {
|
||||
return qs
|
||||
}
|
||||
|
||||
// Disallow newlines to ensure output is a single line.
|
||||
// Only allow printable runes for readability purposes.
|
||||
rawInvalid := func(r rune) bool {
|
||||
return r == '`' || r == '\n' || !unicode.IsPrint(r)
|
||||
}
|
||||
if strings.IndexFunc(s, rawInvalid) < 0 {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
return qs
|
||||
}
|
||||
|
||||
func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string {
|
||||
if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") {
|
||||
return fmt.Sprintf("%v(%v)", t, v)
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
func formatPointer(v reflect.Value, conf FormatConfig) string {
|
||||
p := v.Pointer()
|
||||
if !conf.realPointers {
|
||||
p = 0 // For deterministic printing purposes
|
||||
}
|
||||
s := formatHex(uint64(p))
|
||||
if conf.printType {
|
||||
return fmt.Sprintf("(%v)(%s)", v.Type(), s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func formatHex(u uint64) string {
|
||||
var f string
|
||||
switch {
|
||||
case u <= 0xff:
|
||||
f = "0x%02x"
|
||||
case u <= 0xffff:
|
||||
f = "0x%04x"
|
||||
case u <= 0xffffff:
|
||||
f = "0x%06x"
|
||||
case u <= 0xffffffff:
|
||||
f = "0x%08x"
|
||||
case u <= 0xffffffffff:
|
||||
f = "0x%010x"
|
||||
case u <= 0xffffffffffff:
|
||||
f = "0x%012x"
|
||||
case u <= 0xffffffffffffff:
|
||||
f = "0x%014x"
|
||||
case u <= 0xffffffffffffffff:
|
||||
f = "0x%016x"
|
||||
}
|
||||
return fmt.Sprintf(f, u)
|
||||
}
|
||||
|
||||
// isZero reports whether v is the zero value.
|
||||
// This does not rely on Interface and so can be used on unexported fields.
|
||||
func isZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return v.Bool() == false
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return v.Complex() == 0
|
||||
case reflect.String:
|
||||
return v.String() == ""
|
||||
case reflect.UnsafePointer:
|
||||
return v.Pointer() == 0
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
|
||||
return v.IsNil()
|
||||
case reflect.Array:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if !isZero(v.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !isZero(v.Field(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type visited map[Pointer]bool
|
||||
|
||||
func (m visited) Visit(v reflect.Value) bool {
|
||||
p := PointerOf(v)
|
||||
visited := m[p]
|
||||
m[p] = true
|
||||
return visited
|
||||
}
|
||||
23
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go
generated
vendored
Normal file
23
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2018, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build purego
|
||||
|
||||
package value
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Pointer is an opaque typed pointer and is guaranteed to be comparable.
|
||||
type Pointer struct {
|
||||
p uintptr
|
||||
t reflect.Type
|
||||
}
|
||||
|
||||
// PointerOf returns a Pointer from v, which must be a
|
||||
// reflect.Ptr, reflect.Slice, or reflect.Map.
|
||||
func PointerOf(v reflect.Value) Pointer {
|
||||
// NOTE: Storing a pointer as an uintptr is technically incorrect as it
|
||||
// assumes that the GC implementation does not use a moving collector.
|
||||
return Pointer{v.Pointer(), v.Type()}
|
||||
}
|
||||
26
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go
generated
vendored
Normal file
26
vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go
generated
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2018, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build !purego
|
||||
|
||||
package value
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Pointer is an opaque typed pointer and is guaranteed to be comparable.
|
||||
type Pointer struct {
|
||||
p unsafe.Pointer
|
||||
t reflect.Type
|
||||
}
|
||||
|
||||
// PointerOf returns a Pointer from v, which must be a
|
||||
// reflect.Ptr, reflect.Slice, or reflect.Map.
|
||||
func PointerOf(v reflect.Value) Pointer {
|
||||
// The proper representation of a pointer is unsafe.Pointer,
|
||||
// which is necessary if the GC ever uses a moving collector.
|
||||
return Pointer{unsafe.Pointer(v.Pointer()), v.Type()}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package value
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// SortKeys sorts a list of map keys, deduplicating keys if necessary.
|
||||
// The type of each value must be comparable.
|
||||
func SortKeys(vs []reflect.Value) []reflect.Value {
|
||||
if len(vs) == 0 {
|
||||
return vs
|
||||
}
|
||||
|
||||
// Sort the map keys.
|
||||
sort.Slice(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) })
|
||||
|
||||
// Deduplicate keys (fails for NaNs).
|
||||
vs2 := vs[:1]
|
||||
for _, v := range vs[1:] {
|
||||
if isLess(vs2[len(vs2)-1], v) {
|
||||
vs2 = append(vs2, v)
|
||||
}
|
||||
}
|
||||
return vs2
|
||||
}
|
||||
|
||||
// isLess is a generic function for sorting arbitrary map keys.
|
||||
// The inputs must be of the same type and must be comparable.
|
||||
func isLess(x, y reflect.Value) bool {
|
||||
switch x.Type().Kind() {
|
||||
case reflect.Bool:
|
||||
return !x.Bool() && y.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return x.Int() < y.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return x.Uint() < y.Uint()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fx, fy := x.Float(), y.Float()
|
||||
return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
cx, cy := x.Complex(), y.Complex()
|
||||
rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy)
|
||||
if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) {
|
||||
return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy)
|
||||
}
|
||||
return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry)
|
||||
case reflect.Ptr, reflect.UnsafePointer, reflect.Chan:
|
||||
return x.Pointer() < y.Pointer()
|
||||
case reflect.String:
|
||||
return x.String() < y.String()
|
||||
case reflect.Array:
|
||||
for i := 0; i < x.Len(); i++ {
|
||||
if isLess(x.Index(i), y.Index(i)) {
|
||||
return true
|
||||
}
|
||||
if isLess(y.Index(i), x.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
case reflect.Struct:
|
||||
for i := 0; i < x.NumField(); i++ {
|
||||
if isLess(x.Field(i), y.Field(i)) {
|
||||
return true
|
||||
}
|
||||
if isLess(y.Field(i), x.Field(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
case reflect.Interface:
|
||||
vx, vy := x.Elem(), y.Elem()
|
||||
if !vx.IsValid() || !vy.IsValid() {
|
||||
return !vx.IsValid() && vy.IsValid()
|
||||
}
|
||||
tx, ty := vx.Type(), vy.Type()
|
||||
if tx == ty {
|
||||
return isLess(x.Elem(), y.Elem())
|
||||
}
|
||||
if tx.Kind() != ty.Kind() {
|
||||
return vx.Kind() < vy.Kind()
|
||||
}
|
||||
if tx.String() != ty.String() {
|
||||
return tx.String() < ty.String()
|
||||
}
|
||||
if tx.PkgPath() != ty.PkgPath() {
|
||||
return tx.PkgPath() < ty.PkgPath()
|
||||
}
|
||||
// This can happen in rare situations, so we fallback to just comparing
|
||||
// the unique pointer for a reflect.Type. This guarantees deterministic
|
||||
// ordering within a program, but it is obviously not stable.
|
||||
return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer()
|
||||
default:
|
||||
// Must be Func, Map, or Slice; which are not comparable.
|
||||
panic(fmt.Sprintf("%T is not comparable", x.Type()))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,456 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp/internal/function"
|
||||
)
|
||||
|
||||
// Option configures for specific behavior of Equal and Diff. In particular,
|
||||
// the fundamental Option functions (Ignore, Transformer, and Comparer),
|
||||
// configure how equality is determined.
|
||||
//
|
||||
// The fundamental options may be composed with filters (FilterPath and
|
||||
// FilterValues) to control the scope over which they are applied.
|
||||
//
|
||||
// The cmp/cmpopts package provides helper functions for creating options that
|
||||
// may be used with Equal and Diff.
|
||||
type Option interface {
|
||||
// filter applies all filters and returns the option that remains.
|
||||
// Each option may only read s.curPath and call s.callTTBFunc.
|
||||
//
|
||||
// An Options is returned only if multiple comparers or transformers
|
||||
// can apply simultaneously and will only contain values of those types
|
||||
// or sub-Options containing values of those types.
|
||||
filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption
|
||||
}
|
||||
|
||||
// applicableOption represents the following types:
|
||||
// Fundamental: ignore | invalid | *comparer | *transformer
|
||||
// Grouping: Options
|
||||
type applicableOption interface {
|
||||
Option
|
||||
|
||||
// apply executes the option, which may mutate s or panic.
|
||||
apply(s *state, vx, vy reflect.Value)
|
||||
}
|
||||
|
||||
// coreOption represents the following types:
|
||||
// Fundamental: ignore | invalid | *comparer | *transformer
|
||||
// Filters: *pathFilter | *valuesFilter
|
||||
type coreOption interface {
|
||||
Option
|
||||
isCore()
|
||||
}
|
||||
|
||||
type core struct{}
|
||||
|
||||
func (core) isCore() {}
|
||||
|
||||
// Options is a list of Option values that also satisfies the Option interface.
|
||||
// Helper comparison packages may return an Options value when packing multiple
|
||||
// Option values into a single Option. When this package processes an Options,
|
||||
// it will be implicitly expanded into a flat list.
|
||||
//
|
||||
// Applying a filter on an Options is equivalent to applying that same filter
|
||||
// on all individual options held within.
|
||||
type Options []Option
|
||||
|
||||
func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) {
|
||||
for _, opt := range opts {
|
||||
switch opt := opt.filter(s, vx, vy, t); opt.(type) {
|
||||
case ignore:
|
||||
return ignore{} // Only ignore can short-circuit evaluation
|
||||
case invalid:
|
||||
out = invalid{} // Takes precedence over comparer or transformer
|
||||
case *comparer, *transformer, Options:
|
||||
switch out.(type) {
|
||||
case nil:
|
||||
out = opt
|
||||
case invalid:
|
||||
// Keep invalid
|
||||
case *comparer, *transformer, Options:
|
||||
out = Options{out, opt} // Conflicting comparers or transformers
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (opts Options) apply(s *state, _, _ reflect.Value) {
|
||||
const warning = "ambiguous set of applicable options"
|
||||
const help = "consider using filters to ensure at most one Comparer or Transformer may apply"
|
||||
var ss []string
|
||||
for _, opt := range flattenOptions(nil, opts) {
|
||||
ss = append(ss, fmt.Sprint(opt))
|
||||
}
|
||||
set := strings.Join(ss, "\n\t")
|
||||
panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help))
|
||||
}
|
||||
|
||||
func (opts Options) String() string {
|
||||
var ss []string
|
||||
for _, opt := range opts {
|
||||
ss = append(ss, fmt.Sprint(opt))
|
||||
}
|
||||
return fmt.Sprintf("Options{%s}", strings.Join(ss, ", "))
|
||||
}
|
||||
|
||||
// FilterPath returns a new Option where opt is only evaluated if filter f
|
||||
// returns true for the current Path in the value tree.
|
||||
//
|
||||
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
|
||||
// a previously filtered Option.
|
||||
func FilterPath(f func(Path) bool, opt Option) Option {
|
||||
if f == nil {
|
||||
panic("invalid path filter function")
|
||||
}
|
||||
if opt := normalizeOption(opt); opt != nil {
|
||||
return &pathFilter{fnc: f, opt: opt}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type pathFilter struct {
|
||||
core
|
||||
fnc func(Path) bool
|
||||
opt Option
|
||||
}
|
||||
|
||||
func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
||||
if f.fnc(s.curPath) {
|
||||
return f.opt.filter(s, vx, vy, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f pathFilter) String() string {
|
||||
fn := getFuncName(reflect.ValueOf(f.fnc).Pointer())
|
||||
return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt)
|
||||
}
|
||||
|
||||
// FilterValues returns a new Option where opt is only evaluated if filter f,
|
||||
// which is a function of the form "func(T, T) bool", returns true for the
|
||||
// current pair of values being compared. If the type of the values is not
|
||||
// assignable to T, then this filter implicitly returns false.
|
||||
//
|
||||
// The filter function must be
|
||||
// symmetric (i.e., agnostic to the order of the inputs) and
|
||||
// deterministic (i.e., produces the same result when given the same inputs).
|
||||
// If T is an interface, it is possible that f is called with two values with
|
||||
// different concrete types that both implement T.
|
||||
//
|
||||
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
|
||||
// a previously filtered Option.
|
||||
func FilterValues(f interface{}, opt Option) Option {
|
||||
v := reflect.ValueOf(f)
|
||||
if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
|
||||
panic(fmt.Sprintf("invalid values filter function: %T", f))
|
||||
}
|
||||
if opt := normalizeOption(opt); opt != nil {
|
||||
vf := &valuesFilter{fnc: v, opt: opt}
|
||||
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||
vf.typ = ti
|
||||
}
|
||||
return vf
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type valuesFilter struct {
|
||||
core
|
||||
typ reflect.Type // T
|
||||
fnc reflect.Value // func(T, T) bool
|
||||
opt Option
|
||||
}
|
||||
|
||||
func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
||||
if !vx.IsValid() || !vy.IsValid() {
|
||||
return invalid{}
|
||||
}
|
||||
if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) {
|
||||
return f.opt.filter(s, vx, vy, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f valuesFilter) String() string {
|
||||
fn := getFuncName(f.fnc.Pointer())
|
||||
return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
|
||||
}
|
||||
|
||||
// Ignore is an Option that causes all comparisons to be ignored.
|
||||
// This value is intended to be combined with FilterPath or FilterValues.
|
||||
// It is an error to pass an unfiltered Ignore option to Equal.
|
||||
func Ignore() Option { return ignore{} }
|
||||
|
||||
type ignore struct{ core }
|
||||
|
||||
func (ignore) isFiltered() bool { return false }
|
||||
func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} }
|
||||
func (ignore) apply(_ *state, _, _ reflect.Value) { return }
|
||||
func (ignore) String() string { return "Ignore()" }
|
||||
|
||||
// invalid is a sentinel Option type to indicate that some options could not
|
||||
// be evaluated due to unexported fields.
|
||||
type invalid struct{ core }
|
||||
|
||||
func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} }
|
||||
func (invalid) apply(s *state, _, _ reflect.Value) {
|
||||
const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported"
|
||||
panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
|
||||
}
|
||||
|
||||
// Transformer returns an Option that applies a transformation function that
|
||||
// converts values of a certain type into that of another.
|
||||
//
|
||||
// The transformer f must be a function "func(T) R" that converts values of
|
||||
// type T to those of type R and is implicitly filtered to input values
|
||||
// assignable to T. The transformer must not mutate T in any way.
|
||||
//
|
||||
// To help prevent some cases of infinite recursive cycles applying the
|
||||
// same transform to the output of itself (e.g., in the case where the
|
||||
// input and output types are the same), an implicit filter is added such that
|
||||
// a transformer is applicable only if that exact transformer is not already
|
||||
// in the tail of the Path since the last non-Transform step.
|
||||
// For situations where the implicit filter is still insufficient,
|
||||
// consider using cmpopts.AcyclicTransformer, which adds a filter
|
||||
// to prevent the transformer from being recursively applied upon itself.
|
||||
//
|
||||
// The name is a user provided label that is used as the Transform.Name in the
|
||||
// transformation PathStep. If empty, an arbitrary name is used.
|
||||
func Transformer(name string, f interface{}) Option {
|
||||
v := reflect.ValueOf(f)
|
||||
if !function.IsType(v.Type(), function.Transformer) || v.IsNil() {
|
||||
panic(fmt.Sprintf("invalid transformer function: %T", f))
|
||||
}
|
||||
if name == "" {
|
||||
name = "λ" // Lambda-symbol as place-holder for anonymous transformer
|
||||
}
|
||||
if !isValid(name) {
|
||||
panic(fmt.Sprintf("invalid name: %q", name))
|
||||
}
|
||||
tr := &transformer{name: name, fnc: reflect.ValueOf(f)}
|
||||
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||
tr.typ = ti
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
type transformer struct {
|
||||
core
|
||||
name string
|
||||
typ reflect.Type // T
|
||||
fnc reflect.Value // func(T) R
|
||||
}
|
||||
|
||||
func (tr *transformer) isFiltered() bool { return tr.typ != nil }
|
||||
|
||||
func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
||||
for i := len(s.curPath) - 1; i >= 0; i-- {
|
||||
if t, ok := s.curPath[i].(*transform); !ok {
|
||||
break // Hit most recent non-Transform step
|
||||
} else if tr == t.trans {
|
||||
return nil // Cannot directly use same Transform
|
||||
}
|
||||
}
|
||||
if tr.typ == nil || t.AssignableTo(tr.typ) {
|
||||
return tr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tr *transformer) apply(s *state, vx, vy reflect.Value) {
|
||||
// Update path before calling the Transformer so that dynamic checks
|
||||
// will use the updated path.
|
||||
s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr})
|
||||
defer s.curPath.pop()
|
||||
|
||||
vx = s.callTRFunc(tr.fnc, vx)
|
||||
vy = s.callTRFunc(tr.fnc, vy)
|
||||
s.compareAny(vx, vy)
|
||||
}
|
||||
|
||||
func (tr transformer) String() string {
|
||||
return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer()))
|
||||
}
|
||||
|
||||
// Comparer returns an Option that determines whether two values are equal
|
||||
// to each other.
|
||||
//
|
||||
// The comparer f must be a function "func(T, T) bool" and is implicitly
|
||||
// filtered to input values assignable to T. If T is an interface, it is
|
||||
// possible that f is called with two values of different concrete types that
|
||||
// both implement T.
|
||||
//
|
||||
// The equality function must be:
|
||||
// • Symmetric: equal(x, y) == equal(y, x)
|
||||
// • Deterministic: equal(x, y) == equal(x, y)
|
||||
// • Pure: equal(x, y) does not modify x or y
|
||||
func Comparer(f interface{}) Option {
|
||||
v := reflect.ValueOf(f)
|
||||
if !function.IsType(v.Type(), function.Equal) || v.IsNil() {
|
||||
panic(fmt.Sprintf("invalid comparer function: %T", f))
|
||||
}
|
||||
cm := &comparer{fnc: v}
|
||||
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||
cm.typ = ti
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
type comparer struct {
|
||||
core
|
||||
typ reflect.Type // T
|
||||
fnc reflect.Value // func(T, T) bool
|
||||
}
|
||||
|
||||
func (cm *comparer) isFiltered() bool { return cm.typ != nil }
|
||||
|
||||
func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
||||
if cm.typ == nil || t.AssignableTo(cm.typ) {
|
||||
return cm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *comparer) apply(s *state, vx, vy reflect.Value) {
|
||||
eq := s.callTTBFunc(cm.fnc, vx, vy)
|
||||
s.report(eq, vx, vy)
|
||||
}
|
||||
|
||||
func (cm comparer) String() string {
|
||||
return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer()))
|
||||
}
|
||||
|
||||
// AllowUnexported returns an Option that forcibly allows operations on
|
||||
// unexported fields in certain structs, which are specified by passing in a
|
||||
// value of each struct type.
|
||||
//
|
||||
// Users of this option must understand that comparing on unexported fields
|
||||
// from external packages is not safe since changes in the internal
|
||||
// implementation of some external package may cause the result of Equal
|
||||
// to unexpectedly change. However, it may be valid to use this option on types
|
||||
// defined in an internal package where the semantic meaning of an unexported
|
||||
// field is in the control of the user.
|
||||
//
|
||||
// For some cases, a custom Comparer should be used instead that defines
|
||||
// equality as a function of the public API of a type rather than the underlying
|
||||
// unexported implementation.
|
||||
//
|
||||
// For example, the reflect.Type documentation defines equality to be determined
|
||||
// by the == operator on the interface (essentially performing a shallow pointer
|
||||
// comparison) and most attempts to compare *regexp.Regexp types are interested
|
||||
// in only checking that the regular expression strings are equal.
|
||||
// Both of these are accomplished using Comparers:
|
||||
//
|
||||
// Comparer(func(x, y reflect.Type) bool { return x == y })
|
||||
// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() })
|
||||
//
|
||||
// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
|
||||
// all unexported fields on specified struct types.
|
||||
func AllowUnexported(types ...interface{}) Option {
|
||||
if !supportAllowUnexported {
|
||||
panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS")
|
||||
}
|
||||
m := make(map[reflect.Type]bool)
|
||||
for _, typ := range types {
|
||||
t := reflect.TypeOf(typ)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("invalid struct type: %T", typ))
|
||||
}
|
||||
m[t] = true
|
||||
}
|
||||
return visibleStructs(m)
|
||||
}
|
||||
|
||||
type visibleStructs map[reflect.Type]bool
|
||||
|
||||
func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// reporter is an Option that configures how differences are reported.
|
||||
type reporter interface {
|
||||
// TODO: Not exported yet.
|
||||
//
|
||||
// Perhaps add PushStep and PopStep and change Report to only accept
|
||||
// a PathStep instead of the full-path? Adding a PushStep and PopStep makes
|
||||
// it clear that we are traversing the value tree in a depth-first-search
|
||||
// manner, which has an effect on how values are printed.
|
||||
|
||||
Option
|
||||
|
||||
// Report is called for every comparison made and will be provided with
|
||||
// the two values being compared, the equality result, and the
|
||||
// current path in the value tree. It is possible for x or y to be an
|
||||
// invalid reflect.Value if one of the values is non-existent;
|
||||
// which is possible with maps and slices.
|
||||
Report(x, y reflect.Value, eq bool, p Path)
|
||||
}
|
||||
|
||||
// normalizeOption normalizes the input options such that all Options groups
|
||||
// are flattened and groups with a single element are reduced to that element.
|
||||
// Only coreOptions and Options containing coreOptions are allowed.
|
||||
func normalizeOption(src Option) Option {
|
||||
switch opts := flattenOptions(nil, Options{src}); len(opts) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return opts[0]
|
||||
default:
|
||||
return opts
|
||||
}
|
||||
}
|
||||
|
||||
// flattenOptions copies all options in src to dst as a flat list.
|
||||
// Only coreOptions and Options containing coreOptions are allowed.
|
||||
func flattenOptions(dst, src Options) Options {
|
||||
for _, opt := range src {
|
||||
switch opt := opt.(type) {
|
||||
case nil:
|
||||
continue
|
||||
case Options:
|
||||
dst = flattenOptions(dst, opt)
|
||||
case coreOption:
|
||||
dst = append(dst, opt)
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid option type: %T", opt))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// getFuncName returns a short function name from the pointer.
|
||||
// The string parsing logic works up until Go1.9.
|
||||
func getFuncName(p uintptr) string {
|
||||
fnc := runtime.FuncForPC(p)
|
||||
if fnc == nil {
|
||||
return "<unknown>"
|
||||
}
|
||||
name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm"
|
||||
if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") {
|
||||
// Strip the package name from method name.
|
||||
name = strings.TrimSuffix(name, ")-fm")
|
||||
name = strings.TrimSuffix(name, ")·fm")
|
||||
if i := strings.LastIndexByte(name, '('); i >= 0 {
|
||||
methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc"
|
||||
if j := strings.LastIndexByte(methodName, '.'); j >= 0 {
|
||||
methodName = methodName[j+1:] // E.g., "myfunc"
|
||||
}
|
||||
name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc"
|
||||
}
|
||||
}
|
||||
if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
||||
// Strip the package name.
|
||||
name = name[i+1:] // E.g., "mypkg.(mytype).myfunc"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type (
|
||||
// Path is a list of PathSteps describing the sequence of operations to get
|
||||
// from some root type to the current position in the value tree.
|
||||
// The first Path element is always an operation-less PathStep that exists
|
||||
// simply to identify the initial type.
|
||||
//
|
||||
// When traversing structs with embedded structs, the embedded struct will
|
||||
// always be accessed as a field before traversing the fields of the
|
||||
// embedded struct themselves. That is, an exported field from the
|
||||
// embedded struct will never be accessed directly from the parent struct.
|
||||
Path []PathStep
|
||||
|
||||
// PathStep is a union-type for specific operations to traverse
|
||||
// a value's tree structure. Users of this package never need to implement
|
||||
// these types as values of this type will be returned by this package.
|
||||
PathStep interface {
|
||||
String() string
|
||||
Type() reflect.Type // Resulting type after performing the path step
|
||||
isPathStep()
|
||||
}
|
||||
|
||||
// SliceIndex is an index operation on a slice or array at some index Key.
|
||||
SliceIndex interface {
|
||||
PathStep
|
||||
Key() int // May return -1 if in a split state
|
||||
|
||||
// SplitKeys returns the indexes for indexing into slices in the
|
||||
// x and y values, respectively. These indexes may differ due to the
|
||||
// insertion or removal of an element in one of the slices, causing
|
||||
// all of the indexes to be shifted. If an index is -1, then that
|
||||
// indicates that the element does not exist in the associated slice.
|
||||
//
|
||||
// Key is guaranteed to return -1 if and only if the indexes returned
|
||||
// by SplitKeys are not the same. SplitKeys will never return -1 for
|
||||
// both indexes.
|
||||
SplitKeys() (x int, y int)
|
||||
|
||||
isSliceIndex()
|
||||
}
|
||||
// MapIndex is an index operation on a map at some index Key.
|
||||
MapIndex interface {
|
||||
PathStep
|
||||
Key() reflect.Value
|
||||
isMapIndex()
|
||||
}
|
||||
// TypeAssertion represents a type assertion on an interface.
|
||||
TypeAssertion interface {
|
||||
PathStep
|
||||
isTypeAssertion()
|
||||
}
|
||||
// StructField represents a struct field access on a field called Name.
|
||||
StructField interface {
|
||||
PathStep
|
||||
Name() string
|
||||
Index() int
|
||||
isStructField()
|
||||
}
|
||||
// Indirect represents pointer indirection on the parent type.
|
||||
Indirect interface {
|
||||
PathStep
|
||||
isIndirect()
|
||||
}
|
||||
// Transform is a transformation from the parent type to the current type.
|
||||
Transform interface {
|
||||
PathStep
|
||||
Name() string
|
||||
Func() reflect.Value
|
||||
|
||||
// Option returns the originally constructed Transformer option.
|
||||
// The == operator can be used to detect the exact option used.
|
||||
Option() Option
|
||||
|
||||
isTransform()
|
||||
}
|
||||
)
|
||||
|
||||
func (pa *Path) push(s PathStep) {
|
||||
*pa = append(*pa, s)
|
||||
}
|
||||
|
||||
func (pa *Path) pop() {
|
||||
*pa = (*pa)[:len(*pa)-1]
|
||||
}
|
||||
|
||||
// Last returns the last PathStep in the Path.
|
||||
// If the path is empty, this returns a non-nil PathStep that reports a nil Type.
|
||||
func (pa Path) Last() PathStep {
|
||||
return pa.Index(-1)
|
||||
}
|
||||
|
||||
// Index returns the ith step in the Path and supports negative indexing.
|
||||
// A negative index starts counting from the tail of the Path such that -1
|
||||
// refers to the last step, -2 refers to the second-to-last step, and so on.
|
||||
// If index is invalid, this returns a non-nil PathStep that reports a nil Type.
|
||||
func (pa Path) Index(i int) PathStep {
|
||||
if i < 0 {
|
||||
i = len(pa) + i
|
||||
}
|
||||
if i < 0 || i >= len(pa) {
|
||||
return pathStep{}
|
||||
}
|
||||
return pa[i]
|
||||
}
|
||||
|
||||
// String returns the simplified path to a node.
|
||||
// The simplified path only contains struct field accesses.
|
||||
//
|
||||
// For example:
|
||||
// MyMap.MySlices.MyField
|
||||
func (pa Path) String() string {
|
||||
var ss []string
|
||||
for _, s := range pa {
|
||||
if _, ok := s.(*structField); ok {
|
||||
ss = append(ss, s.String())
|
||||
}
|
||||
}
|
||||
return strings.TrimPrefix(strings.Join(ss, ""), ".")
|
||||
}
|
||||
|
||||
// GoString returns the path to a specific node using Go syntax.
|
||||
//
|
||||
// For example:
|
||||
// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField
|
||||
func (pa Path) GoString() string {
|
||||
var ssPre, ssPost []string
|
||||
var numIndirect int
|
||||
for i, s := range pa {
|
||||
var nextStep PathStep
|
||||
if i+1 < len(pa) {
|
||||
nextStep = pa[i+1]
|
||||
}
|
||||
switch s := s.(type) {
|
||||
case *indirect:
|
||||
numIndirect++
|
||||
pPre, pPost := "(", ")"
|
||||
switch nextStep.(type) {
|
||||
case *indirect:
|
||||
continue // Next step is indirection, so let them batch up
|
||||
case *structField:
|
||||
numIndirect-- // Automatic indirection on struct fields
|
||||
case nil:
|
||||
pPre, pPost = "", "" // Last step; no need for parenthesis
|
||||
}
|
||||
if numIndirect > 0 {
|
||||
ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect))
|
||||
ssPost = append(ssPost, pPost)
|
||||
}
|
||||
numIndirect = 0
|
||||
continue
|
||||
case *transform:
|
||||
ssPre = append(ssPre, s.trans.name+"(")
|
||||
ssPost = append(ssPost, ")")
|
||||
continue
|
||||
case *typeAssertion:
|
||||
// As a special-case, elide type assertions on anonymous types
|
||||
// since they are typically generated dynamically and can be very
|
||||
// verbose. For example, some transforms return interface{} because
|
||||
// of Go's lack of generics, but typically take in and return the
|
||||
// exact same concrete type.
|
||||
if s.Type().PkgPath() == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ssPost = append(ssPost, s.String())
|
||||
}
|
||||
for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 {
|
||||
ssPre[i], ssPre[j] = ssPre[j], ssPre[i]
|
||||
}
|
||||
return strings.Join(ssPre, "") + strings.Join(ssPost, "")
|
||||
}
|
||||
|
||||
type (
|
||||
pathStep struct {
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
sliceIndex struct {
|
||||
pathStep
|
||||
xkey, ykey int
|
||||
}
|
||||
mapIndex struct {
|
||||
pathStep
|
||||
key reflect.Value
|
||||
}
|
||||
typeAssertion struct {
|
||||
pathStep
|
||||
}
|
||||
structField struct {
|
||||
pathStep
|
||||
name string
|
||||
idx int
|
||||
|
||||
// These fields are used for forcibly accessing an unexported field.
|
||||
// pvx, pvy, and field are only valid if unexported is true.
|
||||
unexported bool
|
||||
force bool // Forcibly allow visibility
|
||||
pvx, pvy reflect.Value // Parent values
|
||||
field reflect.StructField // Field information
|
||||
}
|
||||
indirect struct {
|
||||
pathStep
|
||||
}
|
||||
transform struct {
|
||||
pathStep
|
||||
trans *transformer
|
||||
}
|
||||
)
|
||||
|
||||
func (ps pathStep) Type() reflect.Type { return ps.typ }
|
||||
func (ps pathStep) String() string {
|
||||
if ps.typ == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
s := ps.typ.String()
|
||||
if s == "" || strings.ContainsAny(s, "{}\n") {
|
||||
return "root" // Type too simple or complex to print
|
||||
}
|
||||
return fmt.Sprintf("{%s}", s)
|
||||
}
|
||||
|
||||
func (si sliceIndex) String() string {
|
||||
switch {
|
||||
case si.xkey == si.ykey:
|
||||
return fmt.Sprintf("[%d]", si.xkey)
|
||||
case si.ykey == -1:
|
||||
// [5->?] means "I don't know where X[5] went"
|
||||
return fmt.Sprintf("[%d->?]", si.xkey)
|
||||
case si.xkey == -1:
|
||||
// [?->3] means "I don't know where Y[3] came from"
|
||||
return fmt.Sprintf("[?->%d]", si.ykey)
|
||||
default:
|
||||
// [5->3] means "X[5] moved to Y[3]"
|
||||
return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey)
|
||||
}
|
||||
}
|
||||
func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) }
|
||||
func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) }
|
||||
func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) }
|
||||
func (in indirect) String() string { return "*" }
|
||||
func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) }
|
||||
|
||||
func (si sliceIndex) Key() int {
|
||||
if si.xkey != si.ykey {
|
||||
return -1
|
||||
}
|
||||
return si.xkey
|
||||
}
|
||||
func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey }
|
||||
func (mi mapIndex) Key() reflect.Value { return mi.key }
|
||||
func (sf structField) Name() string { return sf.name }
|
||||
func (sf structField) Index() int { return sf.idx }
|
||||
func (tf transform) Name() string { return tf.trans.name }
|
||||
func (tf transform) Func() reflect.Value { return tf.trans.fnc }
|
||||
func (tf transform) Option() Option { return tf.trans }
|
||||
|
||||
func (pathStep) isPathStep() {}
|
||||
func (sliceIndex) isSliceIndex() {}
|
||||
func (mapIndex) isMapIndex() {}
|
||||
func (typeAssertion) isTypeAssertion() {}
|
||||
func (structField) isStructField() {}
|
||||
func (indirect) isIndirect() {}
|
||||
func (transform) isTransform() {}
|
||||
|
||||
var (
|
||||
_ SliceIndex = sliceIndex{}
|
||||
_ MapIndex = mapIndex{}
|
||||
_ TypeAssertion = typeAssertion{}
|
||||
_ StructField = structField{}
|
||||
_ Indirect = indirect{}
|
||||
_ Transform = transform{}
|
||||
|
||||
_ PathStep = sliceIndex{}
|
||||
_ PathStep = mapIndex{}
|
||||
_ PathStep = typeAssertion{}
|
||||
_ PathStep = structField{}
|
||||
_ PathStep = indirect{}
|
||||
_ PathStep = transform{}
|
||||
)
|
||||
|
||||
// isExported reports whether the identifier is exported.
|
||||
func isExported(id string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(id)
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// isValid reports whether the identifier is valid.
|
||||
// Empty and underscore-only strings are not valid.
|
||||
func isValid(id string) bool {
|
||||
ok := id != "" && id != "_"
|
||||
for j, c := range id {
|
||||
ok = ok && (j > 0 || !unicode.IsDigit(c))
|
||||
ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c))
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp/internal/value"
|
||||
)
|
||||
|
||||
type defaultReporter struct {
|
||||
Option
|
||||
diffs []string // List of differences, possibly truncated
|
||||
ndiffs int // Total number of differences
|
||||
nbytes int // Number of bytes in diffs
|
||||
nlines int // Number of lines in diffs
|
||||
}
|
||||
|
||||
var _ reporter = (*defaultReporter)(nil)
|
||||
|
||||
func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) {
|
||||
if eq {
|
||||
return // Ignore equal results
|
||||
}
|
||||
const maxBytes = 4096
|
||||
const maxLines = 256
|
||||
r.ndiffs++
|
||||
if r.nbytes < maxBytes && r.nlines < maxLines {
|
||||
sx := value.Format(x, value.FormatConfig{UseStringer: true})
|
||||
sy := value.Format(y, value.FormatConfig{UseStringer: true})
|
||||
if sx == sy {
|
||||
// Unhelpful output, so use more exact formatting.
|
||||
sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true})
|
||||
sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true})
|
||||
}
|
||||
s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
|
||||
r.diffs = append(r.diffs, s)
|
||||
r.nbytes += len(s)
|
||||
r.nlines += strings.Count(s, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *defaultReporter) String() string {
|
||||
s := strings.Join(r.diffs, "")
|
||||
if r.ndiffs == len(r.diffs) {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs))
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build purego
|
||||
|
||||
package cmp
|
||||
|
||||
import "reflect"
|
||||
|
||||
const supportAllowUnexported = false
|
||||
|
||||
func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value {
|
||||
panic("unsafeRetrieveField is not implemented")
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build !purego
|
||||
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const supportAllowUnexported = true
|
||||
|
||||
// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct
|
||||
// such that the value has read-write permissions.
|
||||
//
|
||||
// The parent struct, v, must be addressable, while f must be a StructField
|
||||
// describing the field to retrieve.
|
||||
func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value {
|
||||
return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem()
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# gotest.tools
|
||||
|
||||
A collection of packages to augment `testing` and support common patterns.
|
||||
|
||||
[](https://godoc.org/gotest.tools)
|
||||
[](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master)
|
||||
[](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`
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
/*Package assert provides assertions for comparing expected values to actual
|
||||
values. When an assertion fails a helpful error message is printed.
|
||||
|
||||
Assert and Check
|
||||
|
||||
Assert() and Check() both accept a Comparison, and fail the test when the
|
||||
comparison fails. The one difference is that Assert() will end the test execution
|
||||
immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()),
|
||||
return the value of the comparison, then proceed with the rest of the test case.
|
||||
|
||||
Example usage
|
||||
|
||||
The example below shows assert used with some common types.
|
||||
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
func TestEverything(t *testing.T) {
|
||||
// booleans
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, !missing)
|
||||
|
||||
// primitives
|
||||
assert.Equal(t, count, 1)
|
||||
assert.Equal(t, msg, "the message")
|
||||
assert.Assert(t, total != 10) // NotEqual
|
||||
|
||||
// errors
|
||||
assert.NilError(t, closer.Close())
|
||||
assert.Error(t, err, "the exact error message")
|
||||
assert.ErrorContains(t, err, "includes this")
|
||||
assert.ErrorType(t, err, os.IsNotExist)
|
||||
|
||||
// complex types
|
||||
assert.DeepEqual(t, result, myStruct{Name: "title"})
|
||||
assert.Assert(t, is.Len(items, 3))
|
||||
assert.Assert(t, len(sequence) != 0) // NotEmpty
|
||||
assert.Assert(t, is.Contains(mapping, "key"))
|
||||
|
||||
// pointers and interface
|
||||
assert.Assert(t, is.Nil(ref))
|
||||
assert.Assert(t, ref != nil) // NotNil
|
||||
}
|
||||
|
||||
Comparisons
|
||||
|
||||
Package https://godoc.org/gotest.tools/assert/cmp provides
|
||||
many common comparisons. Additional comparisons can be written to compare
|
||||
values in other ways. See the example Assert (CustomComparison).
|
||||
|
||||
Automated migration from testify
|
||||
|
||||
gty-migrate-from-testify is a binary which can update source code which uses
|
||||
testify assertions to use the assertions provided by this package.
|
||||
|
||||
See http://bit.do/cmd-gty-migrate-from-testify.
|
||||
|
||||
|
||||
*/
|
||||
package assert // import "gotest.tools/assert"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
gocmp "github.com/google/go-cmp/cmp"
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage.
|
||||
type BoolOrComparison interface{}
|
||||
|
||||
// TestingT is the subset of testing.T used by the assert package.
|
||||
type TestingT interface {
|
||||
FailNow()
|
||||
Fail()
|
||||
Log(args ...interface{})
|
||||
}
|
||||
|
||||
type helperT interface {
|
||||
Helper()
|
||||
}
|
||||
|
||||
const failureMessage = "assertion failed: "
|
||||
|
||||
// nolint: gocyclo
|
||||
func assert(
|
||||
t TestingT,
|
||||
failer func(),
|
||||
argSelector argSelector,
|
||||
comparison BoolOrComparison,
|
||||
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 := "error is not nil: "
|
||||
t.Log(format.WithCustomMessage(failureMessage+msg+check.Error(), 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))
|
||||
}
|
||||
|
||||
if success {
|
||||
return true
|
||||
}
|
||||
failer()
|
||||
return false
|
||||
}
|
||||
|
||||
func runCompareFunc(
|
||||
t TestingT,
|
||||
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 TestingT, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
const stackIndex = 3 // Assert()/Check(), assert(), formatFailureFromBool()
|
||||
const comparisonArgPos = 1
|
||||
args, err := source.CallExprArgs(stackIndex)
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := boolFailureMessage(args[comparisonArgPos])
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
msg = "expression is false"
|
||||
}
|
||||
|
||||
t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...))
|
||||
}
|
||||
|
||||
func boolFailureMessage(expr ast.Expr) (string, error) {
|
||||
if binaryExpr, ok := expr.(*ast.BinaryExpr); ok && binaryExpr.Op == token.NEQ {
|
||||
x, err := source.FormatNode(binaryExpr.X)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
y, err := source.FormatNode(binaryExpr.Y)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return x + " is " + 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
|
||||
}
|
||||
|
||||
formatted, err := source.FormatNode(expr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "expression is false: " + formatted, nil
|
||||
}
|
||||
|
||||
// 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, cmp.Comparison or
|
||||
// error.
|
||||
// When called with a bool the failure message will contain the literal source
|
||||
// code of the expression.
|
||||
// When called with a cmp.Comparison the comparison is responsible for producing
|
||||
// a helpful failure message.
|
||||
// When called with an error a nil value is considered success. A non-nil error
|
||||
// is a failure, and Error() is used as the failure message.
|
||||
func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsFromComparisonCall, comparison, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Check performs a comparison. If the comparison fails the test is marked as
|
||||
// failed, a failure message is logged, and Check returns false. Otherwise returns
|
||||
// true.
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
return assert(t, t.Fail, argsFromComparisonCall, comparison, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NilError fails the test immediately if err is not nil.
|
||||
// This is equivalent to Assert(t, err)
|
||||
func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, err, msgAndArgs...)
|
||||
}
|
||||
|
||||
// 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 for x and y as part
|
||||
// of the failure message to identify the actual and expected values.
|
||||
//
|
||||
// If either x or y are a multi-line string the failure message will include a
|
||||
// unified diff of the two values. If the values only differ by whitespace
|
||||
// the unified diff will be augmented by replacing whitespace characters with
|
||||
// visible characters to identify the whitespace difference.
|
||||
//
|
||||
// This is equivalent to Assert(t, cmp.Equal(x, y)).
|
||||
func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, cmp.Equal(x, y), msgAndArgs...)
|
||||
}
|
||||
|
||||
// DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are
|
||||
// equal and fails the test if they are not equal.
|
||||
//
|
||||
// Package https://godoc.org/gotest.tools/assert/opt provides some additional
|
||||
// commonly used Options.
|
||||
//
|
||||
// This is equivalent to Assert(t, cmp.DeepEqual(x, y)).
|
||||
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, cmp.DeepEqual(x, y, opts...))
|
||||
}
|
||||
|
||||
// Error fails the test if err is nil, or the error message is not the expected
|
||||
// message.
|
||||
// Equivalent to Assert(t, cmp.Error(err, message)).
|
||||
func Error(t TestingT, err error, message string, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, cmp.Error(err, message), msgAndArgs...)
|
||||
}
|
||||
|
||||
// ErrorContains fails the test if err is nil, or the error message does not
|
||||
// contain the expected substring.
|
||||
// Equivalent to Assert(t, cmp.ErrorContains(err, substring)).
|
||||
func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...)
|
||||
}
|
||||
|
||||
// ErrorType fails the test if err is nil, or err is not the expected type.
|
||||
//
|
||||
// Expected can be one of:
|
||||
// a func(error) bool which returns true if the error is the expected type,
|
||||
// an instance of (or a pointer to) a struct of the expected type,
|
||||
// a pointer to an interface the error is expected to implement,
|
||||
// a reflect.Type of the expected struct or interface.
|
||||
//
|
||||
// Equivalent to Assert(t, cmp.ErrorType(err, expected)).
|
||||
func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert(t, t.FailNow, argsAfterT, cmp.ErrorType(err, expected), msgAndArgs...)
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
/*Package cmp provides Comparisons for Assert and Check*/
|
||||
package cmp // import "gotest.tools/assert/cmp"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gotest.tools/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 google/go-cmp (http://bit.do/go-cmp)
|
||||
// and succeeds if the values are equal.
|
||||
//
|
||||
// The comparison can be customized using comparison Options.
|
||||
// Package https://godoc.org/gotest.tools/assert/opt provides some additional
|
||||
// commonly used Options.
|
||||
func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
||||
return func() (result Result) {
|
||||
defer func() {
|
||||
if panicmsg, handled := handleCmpPanic(recover()); handled {
|
||||
result = ResultFailure(panicmsg)
|
||||
}
|
||||
}()
|
||||
diff := cmp.Diff(x, y, opts...)
|
||||
if diff == "" {
|
||||
return ResultSuccess
|
||||
}
|
||||
return multiLineDiffResult(diff)
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
return ResultFailureTemplate(`
|
||||
{{- .Data.x}} (
|
||||
{{- with callArg 0 }}{{ formatNode . }} {{end -}}
|
||||
{{- printf "%T" .Data.x -}}
|
||||
) != {{ .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) Result {
|
||||
return ResultFailureTemplate(`
|
||||
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
|
||||
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}}
|
||||
{{ .Data.diff }}`,
|
||||
map[string]interface{}{"diff": diff})
|
||||
}
|
||||
|
||||
// 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(fmt.Sprintf("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
|
||||
}
|
||||
}
|
||||
|
||||
func formatErrorMessage(err error) string {
|
||||
if _, ok := err.(interface {
|
||||
Cause() error
|
||||
}); 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 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:
|
||||
// a func(error) bool which returns true if the error is the expected type,
|
||||
// an instance of (or a pointer to) a struct of the expected type,
|
||||
// a pointer to an interface the error is expected to implement,
|
||||
// a reflect.Type of the expected struct or interface.
|
||||
func ErrorType(err error, expected interface{}) Comparison {
|
||||
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(fmt.Sprintf("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
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package cmp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"text/template"
|
||||
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
// Result of a Comparison.
|
||||
type Result interface {
|
||||
Success() bool
|
||||
}
|
||||
|
||||
type result struct {
|
||||
success bool
|
||||
message string
|
||||
}
|
||||
|
||||
func (r result) Success() bool {
|
||||
return r.success
|
||||
}
|
||||
|
||||
func (r result) FailureMessage() string {
|
||||
return r.message
|
||||
}
|
||||
|
||||
// ResultSuccess is a constant which is returned by a ComparisonWithResult to
|
||||
// indicate success.
|
||||
var ResultSuccess = result{success: true}
|
||||
|
||||
// ResultFailure returns a failed Result with a failure message.
|
||||
func ResultFailure(message string) Result {
|
||||
return result{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 {
|
||||
success bool
|
||||
template string
|
||||
data map[string]interface{}
|
||||
}
|
||||
|
||||
func (r templatedResult) Success() bool {
|
||||
return r.success
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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]
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
|
||||
"gotest.tools/assert/cmp"
|
||||
"gotest.tools/internal/format"
|
||||
"gotest.tools/internal/source"
|
||||
)
|
||||
|
||||
func runComparison(
|
||||
t TestingT,
|
||||
argSelector argSelector,
|
||||
f cmp.Comparison,
|
||||
msgAndArgs ...interface{},
|
||||
) bool {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
result := f()
|
||||
if result.Success() {
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
func argsAfterT(args []ast.Expr) []ast.Expr {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
return args[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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
/*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/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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package format // import "gotest.tools/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)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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, errors.New("failed to expression in defer")
|
||||
case 1:
|
||||
return defers[0].Call, nil
|
||||
default:
|
||||
return nil, errors.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
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package source // import "gotest.tools/internal/source"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const baseStackIndex = 1
|
||||
|
||||
// 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, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to get call stack")
|
||||
}
|
||||
debug("call stack position: %s:%d", filename, lineNum)
|
||||
|
||||
node, err := getNodeAtLine(filename, lineNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
debug("found node: %s", debugFormatNode{node})
|
||||
|
||||
return getCallExprArgs(node)
|
||||
}
|
||||
|
||||
func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
|
||||
fileset := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse source file: %s", filename)
|
||||
}
|
||||
|
||||
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, errors.Errorf(
|
||||
"failed to find an expression on line %d in %s", lineNum, filename)
|
||||
}
|
||||
|
||||
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 nodePosition(fileset, node).Line == lineNum:
|
||||
matchedNode = node
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return matchedNode
|
||||
}
|
||||
|
||||
// In golang 1.9 the line number changed from being the line where the statement
|
||||
// ended to the line where the statement began.
|
||||
func nodePosition(fileset *token.FileSet, node ast.Node) token.Position {
|
||||
if goVersionBefore19 {
|
||||
return fileset.Position(node.End())
|
||||
}
|
||||
return fileset.Position(node.Pos())
|
||||
}
|
||||
|
||||
var goVersionBefore19 = func() 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
|
||||
}
|
||||
minor, err := strconv.ParseInt(parts[1], 10, 32)
|
||||
return err == nil && parts[0] == "1" && minor < 9
|
||||
}()
|
||||
|
||||
func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
Loading…
Reference in New Issue